Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
22cfb1f0a9 | |||
a942768c8d | |||
e16eefa166 | |||
66458e831c | |||
08700abd4f | |||
2a96f8efac | |||
0863b2fd71 | |||
78a7310a3b | |||
23207b9085 | |||
6f5c21aa8f | |||
888ddce4ef | |||
5b907309e0 | |||
c3a41c0d4e | |||
b92d1d90ad | |||
3f5070df77 | |||
6fd0bf0046 | |||
1f5f1c8e6c | |||
79d9bf4cba |
63
APM.xaml
63
APM.xaml
@ -1,63 +0,0 @@
|
|||||||
<Window x:Class="AnotherReplayReader.APM"
|
|
||||||
x:ClassModifier="internal"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:local="clr-namespace:AnotherReplayReader"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
Title="APM"
|
|
||||||
Height="450"
|
|
||||||
Width="800">
|
|
||||||
<Grid>
|
|
||||||
<Label Margin="20,10,0,0"
|
|
||||||
HorizontalAlignment="Left"
|
|
||||||
Height="30"
|
|
||||||
VerticalAlignment="Top">
|
|
||||||
选择表格之后按 Ctrl+C 可以复制内容
|
|
||||||
</Label>
|
|
||||||
<Button x:Name="_setPlayerButton"
|
|
||||||
Content="设置玩家信息..."
|
|
||||||
Margin="0,19,22,0"
|
|
||||||
VerticalAlignment="Top"
|
|
||||||
Padding="5,2"
|
|
||||||
Visibility="Visible"
|
|
||||||
Click="OnSetPlayerButtonClick"
|
|
||||||
HorizontalAlignment="Right"/>
|
|
||||||
<DataGrid x:Name="_table"
|
|
||||||
Margin="20,40,22,19"
|
|
||||||
Grid.ColumnSpan="2"
|
|
||||||
CanUserSortColumns="True">
|
|
||||||
<DataGrid.Resources>
|
|
||||||
<Style TargetType="DataGridCell">
|
|
||||||
<EventSetter Event="MouseDoubleClick"
|
|
||||||
Handler="OnTableMouseDoubleClick" />
|
|
||||||
</Style>
|
|
||||||
</DataGrid.Resources>
|
|
||||||
<DataGrid.Columns>
|
|
||||||
<DataGridTextColumn Header="项"
|
|
||||||
Binding="{Binding Path=Name}"
|
|
||||||
IsReadOnly="True" />
|
|
||||||
<DataGridTextColumn Header="玩家1"
|
|
||||||
Binding="{Binding Path=Player1Value}"
|
|
||||||
IsReadOnly="True" />
|
|
||||||
<DataGridTextColumn Header="玩家2"
|
|
||||||
Binding="{Binding Path=Player2Value}"
|
|
||||||
IsReadOnly="True" />
|
|
||||||
<DataGridTextColumn Header="玩家3"
|
|
||||||
Binding="{Binding Path=Player3Value}"
|
|
||||||
IsReadOnly="True" />
|
|
||||||
<DataGridTextColumn Header="玩家4"
|
|
||||||
Binding="{Binding Path=Player4Value}"
|
|
||||||
IsReadOnly="True" />
|
|
||||||
<DataGridTextColumn Header="玩家5"
|
|
||||||
Binding="{Binding Path=Player5Value}"
|
|
||||||
IsReadOnly="True" />
|
|
||||||
<DataGridTextColumn Header="玩家6"
|
|
||||||
Binding="{Binding Path=Player6Value}"
|
|
||||||
IsReadOnly="True" />
|
|
||||||
</DataGrid.Columns>
|
|
||||||
</DataGrid>
|
|
||||||
|
|
||||||
</Grid>
|
|
||||||
</Window>
|
|
230
APM.xaml.cs
230
APM.xaml.cs
@ -1,230 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
|
||||||
{
|
|
||||||
internal class DataValue : IComparable<DataValue>, IComparable
|
|
||||||
{
|
|
||||||
public int? NumberValue { get; }
|
|
||||||
public string Value { get; }
|
|
||||||
|
|
||||||
public DataValue(string value)
|
|
||||||
{
|
|
||||||
Value = value;
|
|
||||||
if (int.TryParse(value, out var numberValue))
|
|
||||||
{
|
|
||||||
NumberValue = numberValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString() => Value;
|
|
||||||
|
|
||||||
public int CompareTo(DataValue other)
|
|
||||||
{
|
|
||||||
if (NumberValue.HasValue == other.NumberValue.HasValue)
|
|
||||||
{
|
|
||||||
if (!NumberValue.HasValue)
|
|
||||||
{
|
|
||||||
return Value.CompareTo(other.Value);
|
|
||||||
}
|
|
||||||
return NumberValue.Value.CompareTo(other.NumberValue.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NumberValue.HasValue ? 1 : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int CompareTo(object obj)
|
|
||||||
{
|
|
||||||
if (obj is DataValue other)
|
|
||||||
{
|
|
||||||
return CompareTo(other);
|
|
||||||
}
|
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class DataRow
|
|
||||||
{
|
|
||||||
public string Name { get; }
|
|
||||||
public DataValue Player1Value => _values[0];
|
|
||||||
public DataValue Player2Value => _values[1];
|
|
||||||
public DataValue Player3Value => _values[2];
|
|
||||||
public DataValue Player4Value => _values[3];
|
|
||||||
public DataValue Player5Value => _values[4];
|
|
||||||
public DataValue Player6Value => _values[5];
|
|
||||||
|
|
||||||
private readonly IReadOnlyList<DataValue> _values;
|
|
||||||
|
|
||||||
public DataRow(string name, IEnumerable<string> values)
|
|
||||||
{
|
|
||||||
Name = name;
|
|
||||||
|
|
||||||
if (values.Count() < 6)
|
|
||||||
{
|
|
||||||
values = values.Concat(new string[6 - values.Count()]);
|
|
||||||
}
|
|
||||||
_values = values.Select(x => new DataValue(x)).ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static class DataTableFactory
|
|
||||||
{
|
|
||||||
static public List<DataRow> Get(Replay replay, PlayerIdentity identity)
|
|
||||||
{
|
|
||||||
var list = new List<byte>
|
|
||||||
{
|
|
||||||
0x0F,
|
|
||||||
0x5F,
|
|
||||||
0x12,
|
|
||||||
0x1B,
|
|
||||||
0x48,
|
|
||||||
0x52,
|
|
||||||
0xFC,
|
|
||||||
0xFD,
|
|
||||||
0x01,
|
|
||||||
0x21,
|
|
||||||
0x33,
|
|
||||||
0x34,
|
|
||||||
0x35,
|
|
||||||
0x37,
|
|
||||||
0x47,
|
|
||||||
0xF6,
|
|
||||||
0xF9,
|
|
||||||
0xF5,
|
|
||||||
0xF8,
|
|
||||||
0x2A,
|
|
||||||
0xFA,
|
|
||||||
0xFB,
|
|
||||||
0x07,
|
|
||||||
0x08,
|
|
||||||
0x05,
|
|
||||||
0x06,
|
|
||||||
0x09,
|
|
||||||
0x00,
|
|
||||||
0x0A,
|
|
||||||
0x03,
|
|
||||||
0x04,
|
|
||||||
0x28,
|
|
||||||
0x29,
|
|
||||||
0x0D,
|
|
||||||
0x0E,
|
|
||||||
0x15,
|
|
||||||
0x14,
|
|
||||||
0x36,
|
|
||||||
0x16,
|
|
||||||
0x2C,
|
|
||||||
0x1A,
|
|
||||||
0x4E,
|
|
||||||
0xFE,
|
|
||||||
0xFF,
|
|
||||||
0x32,
|
|
||||||
0x2E,
|
|
||||||
0x2F,
|
|
||||||
0x4B,
|
|
||||||
0x4C,
|
|
||||||
0x02,
|
|
||||||
0x0C,
|
|
||||||
0x10,
|
|
||||||
};
|
|
||||||
|
|
||||||
var dataList = new List<DataRow>
|
|
||||||
{
|
|
||||||
new DataRow("ID", replay.Players.Select(x => x.PlayerName))
|
|
||||||
};
|
|
||||||
if (replay.Type == ReplayType.Lan && identity.IsUsable)
|
|
||||||
{
|
|
||||||
dataList.Add(new DataRow("局域网IP", replay.Players.Select(x => identity.QueryRealNameAndIP(x.PlayerIP))));
|
|
||||||
}
|
|
||||||
|
|
||||||
var commandCounts = replay.GetCommandCounts();
|
|
||||||
foreach (var command in list)
|
|
||||||
{
|
|
||||||
var counts = commandCounts.TryGetValue(command, out var stored) ? stored : new int[replay.Players.Count];
|
|
||||||
dataList.Add(new DataRow(RA3Commands.GetCommandName(command), counts.Select(x => $"{x}")));
|
|
||||||
commandCounts.Remove(command);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var commandCount in commandCounts)
|
|
||||||
{
|
|
||||||
dataList.Add(new DataRow(RA3Commands.GetCommandName(commandCount.Key), commandCount.Value.Select(x => $"{x}")));
|
|
||||||
}
|
|
||||||
|
|
||||||
return dataList;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// <summary>
|
|
||||||
/// APM.xaml 的交互逻辑
|
|
||||||
/// </summary>
|
|
||||||
internal partial class APM : Window
|
|
||||||
{
|
|
||||||
private PlayerIdentity _identity;
|
|
||||||
|
|
||||||
public APM(Replay replay, PlayerIdentity identity)
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
|
|
||||||
_identity = identity;
|
|
||||||
|
|
||||||
if (_identity.IsUsable)
|
|
||||||
{
|
|
||||||
Dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
_setPlayerButton.IsEnabled = true;
|
|
||||||
_setPlayerButton.Visibility = Visibility.Visible;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
_setPlayerButton.IsEnabled = false;
|
|
||||||
_setPlayerButton.Visibility = Visibility.Hidden;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Task.Run(() =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var dataList = DataTableFactory.Get(replay, _identity);
|
|
||||||
Dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
_table.Items.Clear();
|
|
||||||
foreach (var row in dataList)
|
|
||||||
{
|
|
||||||
_table.Items.Add(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
_table.Columns[1].Header = dataList[0].Player1Value;
|
|
||||||
_table.Columns[2].Header = dataList[0].Player2Value;
|
|
||||||
_table.Columns[3].Header = dataList[0].Player3Value;
|
|
||||||
_table.Columns[4].Header = dataList[0].Player4Value;
|
|
||||||
_table.Columns[5].Header = dataList[0].Player5Value;
|
|
||||||
_table.Columns[6].Header = dataList[0].Player6Value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
MessageBox.Show($"加载录像信息失败:\r\n{e}");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnSetPlayerButtonClick(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
var window1 = new Window1(_identity);
|
|
||||||
window1.ShowDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnTableMouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
|
||||||
{
|
|
||||||
_table.SelectAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
91
About.xaml
91
About.xaml
@ -1,4 +1,5 @@
|
|||||||
<Window x:Class="AnotherReplayReader.About"
|
<Window x:Class="AnotherReplayReader.About"
|
||||||
|
x:ClassModifier="internal"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
@ -6,28 +7,76 @@
|
|||||||
xmlns:local="clr-namespace:AnotherReplayReader"
|
xmlns:local="clr-namespace:AnotherReplayReader"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="About"
|
Title="About"
|
||||||
Height="301.893"
|
Height="420"
|
||||||
Width="429">
|
Width="450"
|
||||||
<Grid Margin="0,0,-8,-59">
|
WindowStartupLocation="CenterOwner"
|
||||||
|
Loaded="OnAboutWindowLoaded">
|
||||||
|
<Grid Margin="0,0,0,-50">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="280"/>
|
<RowDefinition Height="420" />
|
||||||
<RowDefinition Height="50"/>
|
<RowDefinition />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<TextBox x:Name="_idBox" HorizontalAlignment="Left" Margin="54,23,0,10" TextWrapping="Wrap" Text="TextBox" Width="300" BorderBrush="White" RenderTransformOrigin="0.497,0.438" Grid.Row="1"/>
|
<StackPanel Grid.Row="0"
|
||||||
<TextBlock x:Name="textBlock" Margin="50,10,45,32" TextWrapping="Wrap" Grid.RowSpan="2">
|
Orientation="Vertical">
|
||||||
<Run Text="【自动录像机0.6】"/><LineBreak/>
|
<StackPanel x:Name="_updatePanel"
|
||||||
<Run Text="本工具目前额外支持以下mod的读取:AR、日冕、大蜗牛、Ins、FS、WOP、Eisenreich、TNW"/><LineBreak/>
|
Orientation="Vertical"
|
||||||
<Run Text="当mod增加新的阵营时,会出现未知阵营和阵营错乱现象"/><LineBreak/>
|
Visibility="Collapsed">
|
||||||
<Run Text="有任何问题可以先去找苏醒或节操"/><LineBreak/>
|
<TextBlock x:Name="_updateInfo"
|
||||||
<Run Text="解析录像的代码主要来源于louisdx的研究:"/><LineBreak/>
|
Margin="24,16">
|
||||||
<Hyperlink NavigateUri="https://github.com/louisdx/cnc-replayreaders"><Run Text="https://github.com/louisdx/cnc-replayreaders"/></Hyperlink><LineBreak/>
|
<Run FontWeight="Bold">已经有新版本了呢!</Run>
|
||||||
<Run Text="解析Big的代码来源于OpenSage:"/><LineBreak/>
|
<LineBreak />
|
||||||
<Hyperlink NavigateUri="https://github.com/OpenSAGE/OpenSAGE"><Run Text="https://github.com/OpenSAGE/OpenSAGE"/></Hyperlink><LineBreak/>
|
</TextBlock>
|
||||||
<Run Text="解析Tga的代码来源于Pfim:"/><LineBreak/>
|
<Separator />
|
||||||
<Hyperlink NavigateUri="https://github.com/nickbabcock/Pfim"><Run Text="https://github.com/nickbabcock/Pfim"/></Hyperlink><LineBreak/>
|
</StackPanel>
|
||||||
<Run Text="RA3吧:"/><LineBreak/>
|
<StackPanel Orientation="Vertical"
|
||||||
<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/>
|
Margin="24,16">
|
||||||
<Run Text="ARmod群号:656507961"/></TextBlock>
|
<TextBlock TextWrapping="Wrap">
|
||||||
<TextBlock x:Name="textBlock1" HorizontalAlignment="Left" Margin="10,23,0,11" TextWrapping="Wrap" Width="60" Grid.Row="1"><Run Text="ID"/><LineBreak/><Run/></TextBlock>
|
<Run Text="{Binding Source={x:Static local:App.NameWithVersion},
|
||||||
|
Mode=OneWay,
|
||||||
|
StringFormat={}【{0}】}" />
|
||||||
|
<LineBreak />
|
||||||
|
这个工具目前额外支持以下 Mod 的读取:AR、日冕、大蜗牛、Ins、FS、WOP、Eisenreich、TNW<LineBreak />
|
||||||
|
有任何问题可以先去找苏醒或节操问题(<LineBreak />
|
||||||
|
解析录像的代码主要来源于
|
||||||
|
<Hyperlink NavigateUri="https://www.gamereplays.org/community/index.php?showtopic=706067">R Schneider 的研究</Hyperlink>
|
||||||
|
以及 BoolBada 的
|
||||||
|
<Hyperlink NavigateUri="https://github.com/forcecore/KWReplayAutoSaver">KWReplayAutoSaver</Hyperlink>
|
||||||
|
<LineBreak />
|
||||||
|
解析 Big 的代码来源于
|
||||||
|
<Hyperlink NavigateUri="https://github.com/Qibbi">Jana Mohn</Hyperlink>
|
||||||
|
的 TechnologyAssembler<LineBreak />
|
||||||
|
解析 Tga 的代码来源于
|
||||||
|
<Hyperlink NavigateUri="https://github.com/nickbabcock/Pfim">Pfim</Hyperlink><LineBreak />
|
||||||
|
使用了
|
||||||
|
<Hyperlink NavigateUri="https://github.com/2881099/NPinyin">NPinyin</Hyperlink>
|
||||||
|
以支持按照拼音来查询信息<LineBreak />
|
||||||
|
APM 图表是通过
|
||||||
|
<Hyperlink NavigateUri="https://oxyplot.github.io/">OxyPlot</Hyperlink>
|
||||||
|
画出来的<LineBreak />
|
||||||
|
<LineBreak />
|
||||||
|
欢迎来到红警3吧:
|
||||||
|
<Hyperlink NavigateUri="https://tieba.baidu.com/f?kw=%BA%EC%BE%AF3">https://tieba.baidu.com/ra3</Hyperlink><LineBreak />
|
||||||
|
<Run Text="ARMod 群号:161660710" />
|
||||||
|
</TextBlock>
|
||||||
|
<CheckBox x:Name="_checkForUpdates"
|
||||||
|
Margin="0,16,0,0"
|
||||||
|
Content="自动检查更新"
|
||||||
|
Checked="OnCheckForUpdatesCheckedChanged"
|
||||||
|
Unchecked="OnCheckForUpdatesCheckedChanged" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<DockPanel x:Name="_bottom"
|
||||||
|
Grid.Row="1">
|
||||||
|
<Label DockPanel.Dock="Left"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Content="ID" />
|
||||||
|
<TextBox x:Name="_idBox"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Text="TextBox"
|
||||||
|
BorderBrush="White" />
|
||||||
|
</DockPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
@ -1,34 +1,68 @@
|
|||||||
using System;
|
using AnotherReplayReader.Utils;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
|
||||||
using System.Windows.Data;
|
|
||||||
using System.Windows.Documents;
|
using System.Windows.Documents;
|
||||||
using System.Windows.Input;
|
|
||||||
using System.Windows.Media;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using System.Windows.Shapes;
|
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
namespace AnotherReplayReader
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// About.xaml 的交互逻辑
|
/// About.xaml 的交互逻辑
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class About : Window
|
internal partial class About : Window
|
||||||
{
|
{
|
||||||
public About()
|
private readonly Cache _cache;
|
||||||
|
private readonly UpdateCheckerVersionData? _updateData;
|
||||||
|
|
||||||
|
public About(Cache cache)
|
||||||
{
|
{
|
||||||
|
_cache = cache;
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_idBox.Text = Auth.ID;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e)
|
public About(Cache cache, UpdateCheckerVersionData updateData)
|
||||||
|
{
|
||||||
|
_cache = cache;
|
||||||
|
_updateData = updateData;
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnAboutWindowLoaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
foreach (var hyperlink in this.FindVisualChildren<Hyperlink>())
|
||||||
|
{
|
||||||
|
hyperlink.RequestNavigate += OnHyperlinkRequestNavigate;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _cache.Initialization;
|
||||||
|
_checkForUpdates.IsChecked = _cache.GetOrDefault(UpdateChecker.CheckForUpdatesKey, false);
|
||||||
|
var data = _updateData
|
||||||
|
?? _cache.GetOrDefault<UpdateCheckerVersionData?>(UpdateChecker.CachedDataKey, null);
|
||||||
|
if (data is { } updateData && updateData.IsNewVersion())
|
||||||
|
{
|
||||||
|
_updatePanel.Visibility = Visibility.Visible;
|
||||||
|
_updateInfo.Inlines.Add(updateData.Description);
|
||||||
|
_updateInfo.Inlines.Add(new LineBreak());
|
||||||
|
updateData.Urls.Select(u =>
|
||||||
|
{
|
||||||
|
var h = new Hyperlink();
|
||||||
|
h.Inlines.Add(u);
|
||||||
|
h.NavigateUri = new(u);
|
||||||
|
h.RequestNavigate += OnHyperlinkRequestNavigate;
|
||||||
|
return h;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnHyperlinkRequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e)
|
||||||
{
|
{
|
||||||
Process.Start(e.Uri.ToString());
|
Process.Start(e.Uri.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void OnCheckForUpdatesCheckedChanged(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_cache.Set(UpdateChecker.CheckForUpdatesKey, _checkForUpdates.IsChecked is true);
|
||||||
|
await _cache.Save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,193 +1,63 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
|
||||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
|
||||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
|
||||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
|
||||||
<ProjectGuid>{A54AEAB3-D99C-4E29-8C47-3DFD5B1A0FDE}</ProjectGuid>
|
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<RootNamespace>AnotherReplayReader</RootNamespace>
|
<TargetFramework>net461</TargetFramework>
|
||||||
<AssemblyName>AnotherReplayReader</AssemblyName>
|
|
||||||
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
|
|
||||||
<FileAlignment>512</FileAlignment>
|
|
||||||
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
|
||||||
<WarningLevel>4</WarningLevel>
|
|
||||||
<Deterministic>true</Deterministic>
|
|
||||||
<TargetFrameworkProfile />
|
|
||||||
<IsWebBootstrapper>false</IsWebBootstrapper>
|
|
||||||
<PublishUrl>publish\</PublishUrl>
|
|
||||||
<Install>true</Install>
|
|
||||||
<InstallFrom>Disk</InstallFrom>
|
|
||||||
<UpdateEnabled>false</UpdateEnabled>
|
|
||||||
<UpdateMode>Foreground</UpdateMode>
|
|
||||||
<UpdateInterval>7</UpdateInterval>
|
|
||||||
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
|
|
||||||
<UpdatePeriodically>false</UpdatePeriodically>
|
|
||||||
<UpdateRequired>false</UpdateRequired>
|
|
||||||
<MapFileExtensions>true</MapFileExtensions>
|
|
||||||
<ApplicationRevision>1</ApplicationRevision>
|
|
||||||
<ApplicationVersion>0.0.1.%2a</ApplicationVersion>
|
|
||||||
<UseApplicationTrust>false</UseApplicationTrust>
|
|
||||||
<PublishWizardCompleted>true</PublishWizardCompleted>
|
|
||||||
<BootstrapperEnabled>true</BootstrapperEnabled>
|
|
||||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||||
|
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
|
||||||
|
<PublishUrl>publish\</PublishUrl>
|
||||||
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<IncludePackageReferencesDuringMarkupCompilation>true</IncludePackageReferencesDuringMarkupCompilation>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
<PropertyGroup>
|
||||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
<DebugSymbols>true</DebugSymbols>
|
<OutputPath>bin\$(Configuration)\</OutputPath>
|
||||||
<DebugType>full</DebugType>
|
<LangVersion>latest</LangVersion>
|
||||||
<Optimize>false</Optimize>
|
<Nullable>enable</Nullable>
|
||||||
<OutputPath>bin\Debug\</OutputPath>
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
|
||||||
<ErrorReport>prompt</ErrorReport>
|
|
||||||
<WarningLevel>4</WarningLevel>
|
|
||||||
<Prefer32Bit>false</Prefer32Bit>
|
|
||||||
<LangVersion>7.2</LangVersion>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
|
||||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
|
||||||
<DebugType>pdbonly</DebugType>
|
|
||||||
<Optimize>true</Optimize>
|
|
||||||
<OutputPath>bin\Release\</OutputPath>
|
|
||||||
<DefineConstants>TRACE</DefineConstants>
|
|
||||||
<ErrorReport>prompt</ErrorReport>
|
|
||||||
<WarningLevel>4</WarningLevel>
|
|
||||||
<Prefer32Bit>false</Prefer32Bit>
|
|
||||||
<LangVersion>7.2</LangVersion>
|
|
||||||
<DocumentationFile>
|
|
||||||
</DocumentationFile>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup>
|
|
||||||
<ManifestCertificateThumbprint>DB7DFD435909EF54AE3424563219E8449DA47901</ManifestCertificateThumbprint>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup>
|
|
||||||
<ManifestKeyFile>AnotherReplayReader_TemporaryKey.pfx</ManifestKeyFile>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup>
|
|
||||||
<GenerateManifests>true</GenerateManifests>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup>
|
|
||||||
<SignManifests>false</SignManifests>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup />
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="System" />
|
<Compile Remove="publish\**" />
|
||||||
<Reference Include="System.Data" />
|
<EmbeddedResource Remove="publish\**" />
|
||||||
|
<None Remove="publish\**" />
|
||||||
|
<Page Remove="publish\**" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
<Reference Include="System.Web" />
|
<Reference Include="System.Web" />
|
||||||
<Reference Include="System.Web.Extensions" />
|
<Reference Include="TechnologyAssembler.Core">
|
||||||
<Reference Include="System.Xaml" />
|
<HintPath>TechnologyAssembler.Core.dll</HintPath>
|
||||||
<Reference Include="System.Xml" />
|
<Private>true</Private>
|
||||||
<Reference Include="System.Core" />
|
</Reference>
|
||||||
<Reference Include="System.Xml.Linq" />
|
|
||||||
<Reference Include="System.Data.DataSetExtensions" />
|
|
||||||
<Reference Include="WindowsBase" />
|
|
||||||
<Reference Include="PresentationCore" />
|
|
||||||
<Reference Include="PresentationFramework" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ILMerge">
|
<PackageReference Include="NPinyin.Core" Version="3.0.0" />
|
||||||
<Version>3.0.29</Version>
|
<PackageReference Include="OxyPlot.Wpf" Version="2.1.0" />
|
||||||
</PackageReference>
|
<PackageReference Include="Pfim" Version="0.10.1" />
|
||||||
<PackageReference Include="ILMerge.MSBuild.Task">
|
<PackageReference Include="System.Collections.Immutable" Version="5.0.0" />
|
||||||
<Version>1.0.7</Version>
|
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.0-preview.7.24405.7" />
|
||||||
</PackageReference>
|
<PackageReference Include="System.Text.Json" Version="5.0.2" />
|
||||||
<PackageReference Include="OpenSage.FileFormats.Big">
|
|
||||||
<Version>1.0.0</Version>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Pfim">
|
|
||||||
<Version>0.7.0</Version>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ApplicationDefinition Include="App.xaml">
|
|
||||||
<Generator>MSBuild:Compile</Generator>
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
</ApplicationDefinition>
|
|
||||||
<Compile Include="About.xaml.cs">
|
|
||||||
<DependentUpon>About.xaml</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="APM.xaml.cs">
|
|
||||||
<DependentUpon>APM.xaml</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="Auth.cs" />
|
|
||||||
<Compile Include="Cache.cs" />
|
|
||||||
<Compile Include="BigMinimapCache.cs" />
|
|
||||||
<Compile Include="Debug.xaml.cs">
|
|
||||||
<DependentUpon>Debug.xaml</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="MinimapReader.cs" />
|
|
||||||
<Compile Include="ModData.cs" />
|
|
||||||
<Compile Include="PlayerIdentity.cs" />
|
|
||||||
<Compile Include="Replay.cs" />
|
|
||||||
<Compile Include="Window1.xaml.cs">
|
|
||||||
<DependentUpon>Window1.xaml</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
<Page Include="About.xaml">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
<Generator>MSBuild:Compile</Generator>
|
|
||||||
</Page>
|
|
||||||
<Page Include="APM.xaml">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
<Generator>MSBuild:Compile</Generator>
|
|
||||||
</Page>
|
|
||||||
<Page Include="Debug.xaml">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
<Generator>MSBuild:Compile</Generator>
|
|
||||||
</Page>
|
|
||||||
<Page Include="MainWindow.xaml">
|
|
||||||
<Generator>MSBuild:Compile</Generator>
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
</Page>
|
|
||||||
<Compile Include="App.xaml.cs">
|
|
||||||
<DependentUpon>App.xaml</DependentUpon>
|
|
||||||
<SubType>Code</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="CommandChunk.cs" />
|
|
||||||
<Compile Include="MainWindow.xaml.cs">
|
|
||||||
<DependentUpon>MainWindow.xaml</DependentUpon>
|
|
||||||
<SubType>Code</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Page Include="Window1.xaml">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
<Generator>MSBuild:Compile</Generator>
|
|
||||||
</Page>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="Properties\AssemblyInfo.cs">
|
<Compile Update="Properties\Settings.Designer.cs">
|
||||||
<SubType>Code</SubType>
|
<DesignTimeSharedInput>True</DesignTimeSharedInput>
|
||||||
</Compile>
|
|
||||||
<Compile Include="Properties\Resources.Designer.cs">
|
|
||||||
<AutoGen>True</AutoGen>
|
|
||||||
<DesignTime>True</DesignTime>
|
|
||||||
<DependentUpon>Resources.resx</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="Properties\Settings.Designer.cs">
|
|
||||||
<AutoGen>True</AutoGen>
|
<AutoGen>True</AutoGen>
|
||||||
<DependentUpon>Settings.settings</DependentUpon>
|
<DependentUpon>Settings.settings</DependentUpon>
|
||||||
<DesignTimeSharedInput>True</DesignTimeSharedInput>
|
|
||||||
</Compile>
|
</Compile>
|
||||||
<EmbeddedResource Include="Properties\Resources.resx">
|
</ItemGroup>
|
||||||
<Generator>ResXFileCodeGenerator</Generator>
|
<ItemGroup>
|
||||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
<None Update="Properties\Settings.settings">
|
||||||
</EmbeddedResource>
|
|
||||||
<None Include="app.config" />
|
|
||||||
<None Include="Properties\Settings.settings">
|
|
||||||
<Generator>SettingsSingleFileGenerator</Generator>
|
<Generator>SettingsSingleFileGenerator</Generator>
|
||||||
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
|
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<Target Name="CustomAfterBuild" AfterTargets="Build">
|
||||||
<BootstrapperPackage Include=".NETFramework,Version=v4.6.1">
|
<ItemGroup>
|
||||||
<Visible>False</Visible>
|
<_FilesToMove Include="$(OutputPath)*.dll" />
|
||||||
<ProductName>Microsoft .NET Framework 4.6.1 %28x86 和 x64%29</ProductName>
|
</ItemGroup>
|
||||||
<Install>true</Install>
|
<Message Text="_FilesToMove: @(_FilesToMove->'%(Filename)%(Extension)')" Importance="high" />
|
||||||
</BootstrapperPackage>
|
<Message Text="DestFiles:
 @(_FilesToMove->'$(OutputPath)$(ProjectName)Data\%(Filename)%(Extension)')" Importance="high" />
|
||||||
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
|
<Move SourceFiles="@(_FilesToMove)" DestinationFiles="@(_FilesToMove->'$(OutputPath)$(ProjectName)Data\%(Filename)%(Extension)')" />
|
||||||
<Visible>False</Visible>
|
</Target>
|
||||||
<ProductName>.NET Framework 3.5 SP1</ProductName>
|
|
||||||
<Install>false</Install>
|
|
||||||
</BootstrapperPackage>
|
|
||||||
</ItemGroup>
|
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
|
||||||
</Project>
|
</Project>
|
@ -1,9 +1,9 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 16
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 16.0.30907.101
|
VisualStudioVersion = 17.10.34928.147
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnotherReplayReader", "AnotherReplayReader.csproj", "{A54AEAB3-D99C-4E29-8C47-3DFD5B1A0FDE}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AnotherReplayReader", "AnotherReplayReader.csproj", "{A54AEAB3-D99C-4E29-8C47-3DFD5B1A0FDE}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
239
Apm/ApmPlotter.cs
Normal file
239
Apm/ApmPlotter.cs
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
using AnotherReplayReader.ReplayFile;
|
||||||
|
using OxyPlot;
|
||||||
|
using OxyPlot.Axes;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.Apm
|
||||||
|
{
|
||||||
|
public class ApmPlotterFilterOptions
|
||||||
|
{
|
||||||
|
public bool CountUnknowns { get; }
|
||||||
|
public bool CountAutos { get; }
|
||||||
|
public bool CountClicks { get; }
|
||||||
|
|
||||||
|
public ApmPlotterFilterOptions(bool countUnknowns, bool countAutos, bool countClicks)
|
||||||
|
{
|
||||||
|
CountUnknowns = countUnknowns;
|
||||||
|
CountAutos = countAutos;
|
||||||
|
CountClicks = countClicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShouldSkip(byte command)
|
||||||
|
{
|
||||||
|
if (!CountUnknowns && ApmPlotter.IsUnknown(command))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!CountAutos && ApmPlotter.IsAuto(command))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!CountClicks && ApmPlotter.IsClick(command))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class ApmPlotter
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentDictionary<byte, byte> UnknownCommands;
|
||||||
|
private static readonly ConcurrentDictionary<byte, byte> AutoCommands;
|
||||||
|
private readonly ImmutableArray<(TimeSpan, ImmutableArray<CommandChunk>)> _commands;
|
||||||
|
|
||||||
|
public Replay Replay { get; }
|
||||||
|
public ImmutableArray<Player> Players { get; }
|
||||||
|
public ImmutableArray<TimeSpan> PlayerLifes { get; }
|
||||||
|
public ImmutableArray<TimeSpan> StricterPlayerLifes { get; }
|
||||||
|
public TimeSpan ReplayLength { get; }
|
||||||
|
|
||||||
|
static ApmPlotter()
|
||||||
|
{
|
||||||
|
static KeyValuePair<byte, byte> Create(byte x) => new(x, default);
|
||||||
|
UnknownCommands = new(RA3Commands.UnknownCommands.Select(Create));
|
||||||
|
AutoCommands = new(RA3Commands.AutoCommands.Select(Create));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApmPlotter(Replay replay)
|
||||||
|
{
|
||||||
|
if (replay.Body is not { } body)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Replay must be fully parsed!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Replay = replay;
|
||||||
|
|
||||||
|
// player list without post Commentator
|
||||||
|
Players = (replay.Players.Last().PlayerName == Replay.PostCommentator
|
||||||
|
? replay.Players.Take(replay.Players.Length - 1)
|
||||||
|
: replay.Players).ToImmutableArray();
|
||||||
|
|
||||||
|
// get all commands
|
||||||
|
IEnumerable<CommandChunk> Filter(ReplayChunk chunk)
|
||||||
|
{
|
||||||
|
foreach (var command in CommandChunk.Parse(chunk))
|
||||||
|
{
|
||||||
|
if (command.PlayerIndex >= Players.Length)
|
||||||
|
{
|
||||||
|
Debug.Instance.DebugMessage += $"Unknown command with invalid player index: {command}\r\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
yield return command;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var query = from chunk in body
|
||||||
|
where chunk.Type is 1
|
||||||
|
select (chunk.Time, Filter(chunk).ToImmutableArray());
|
||||||
|
_commands = query.ToImmutableArray();
|
||||||
|
|
||||||
|
var playerLifes = new TimeSpan[Players.Length];
|
||||||
|
var threeSeconds = TimeSpan.FromSeconds(3);
|
||||||
|
var stricterLifes = new TimeSpan[Players.Length];
|
||||||
|
foreach (var (time, commands) in _commands)
|
||||||
|
{
|
||||||
|
var estimatedTime = time + threeSeconds;
|
||||||
|
foreach (var command in commands)
|
||||||
|
{
|
||||||
|
var commandId = command.CommandId;
|
||||||
|
var playerIndex = command.PlayerIndex;
|
||||||
|
if (commandId == 0x21)
|
||||||
|
{
|
||||||
|
if (playerLifes[playerIndex] < estimatedTime)
|
||||||
|
{
|
||||||
|
playerLifes[playerIndex] = estimatedTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!IsUnknown(commandId) && !IsAuto(commandId) && playerIndex >= 0)
|
||||||
|
{
|
||||||
|
if (stricterLifes[playerIndex] < estimatedTime)
|
||||||
|
{
|
||||||
|
stricterLifes[playerIndex] = estimatedTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PlayerLifes = playerLifes.ToImmutableArray();
|
||||||
|
StricterPlayerLifes = stricterLifes.ToImmutableArray();
|
||||||
|
ReplayLength = replay.Footer?.ReplayLength ?? PlayerLifes.Max();
|
||||||
|
}
|
||||||
|
|
||||||
|
public DataPoint[][] GetPoints(TimeSpan resolution, ApmPlotterFilterOptions options)
|
||||||
|
{
|
||||||
|
var lists = Players
|
||||||
|
.Select(x => new List<int> { 0 })
|
||||||
|
.ToArray();
|
||||||
|
var currentTime = TimeSpan.Zero;
|
||||||
|
var currentIndex = 0;
|
||||||
|
foreach (var (time, commands) in _commands)
|
||||||
|
{
|
||||||
|
while (time >= currentTime + resolution)
|
||||||
|
{
|
||||||
|
currentTime += resolution;
|
||||||
|
++currentIndex;
|
||||||
|
}
|
||||||
|
foreach (var command in commands)
|
||||||
|
{
|
||||||
|
if (options.ShouldSkip(command.CommandId) || command.PlayerIndex < 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var list = lists[command.PlayerIndex];
|
||||||
|
var gap = currentIndex - list.Count;
|
||||||
|
if (gap >= 0)
|
||||||
|
{
|
||||||
|
list.AddRange(Enumerable.Repeat(0, gap + 1));
|
||||||
|
}
|
||||||
|
++lists[command.PlayerIndex][currentIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var apmMultiplier = 1 / resolution.TotalMinutes;
|
||||||
|
DataPoint Create(int v, int i) => new(TimeSpanAxis.ToDouble(resolution) * i,
|
||||||
|
v * apmMultiplier);
|
||||||
|
return lists.Select(l => l.Select(Create).ToArray()).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public double[] CalculateAverageApm(ApmPlotterFilterOptions options)
|
||||||
|
{
|
||||||
|
var apm = new double[Players.Length];
|
||||||
|
foreach (var (_, commands) in _commands)
|
||||||
|
{
|
||||||
|
foreach (var command in commands)
|
||||||
|
{
|
||||||
|
if (options.ShouldSkip(command.CommandId) || command.PlayerIndex < 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
++apm[command.PlayerIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var i = 0; i < apm.Length; ++i)
|
||||||
|
{
|
||||||
|
apm[i] /= PlayerLifes[i].TotalMinutes;
|
||||||
|
}
|
||||||
|
return apm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double[] CalculateInstantApm(DataPoint[][] data, TimeSpan begin, TimeSpan end)
|
||||||
|
{
|
||||||
|
return data.Select(playerData =>
|
||||||
|
{
|
||||||
|
return playerData
|
||||||
|
.SkipWhile(p => TimeSpanAxis.ToTimeSpan(p.X) < begin)
|
||||||
|
.TakeWhile(p => TimeSpanAxis.ToTimeSpan(p.X) < end)
|
||||||
|
.Average(p => new double?(p.Y)) ?? double.NaN;
|
||||||
|
}).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<byte, int[]> GetCommandCounts(TimeSpan begin, TimeSpan end)
|
||||||
|
{
|
||||||
|
var playerCommands = new Dictionary<byte, int[]>();
|
||||||
|
|
||||||
|
foreach (var (time, commands) in _commands)
|
||||||
|
{
|
||||||
|
if (time < begin)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (time >= end)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
foreach (var command in commands)
|
||||||
|
{
|
||||||
|
if (!playerCommands.TryGetValue(command.CommandId, out var commandCount))
|
||||||
|
{
|
||||||
|
commandCount = playerCommands[command.CommandId] = new int[Players.Length];
|
||||||
|
}
|
||||||
|
if (command.PlayerIndex >= 0 && command.PlayerIndex < Players.Length)
|
||||||
|
{
|
||||||
|
commandCount[command.PlayerIndex] = commandCount[command.PlayerIndex] + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return playerCommands;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsUnknown(byte commandId)
|
||||||
|
{
|
||||||
|
if (UnknownCommands.ContainsKey(commandId))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (RA3Commands.IsUnknownCommand(commandId))
|
||||||
|
{
|
||||||
|
UnknownCommands.TryAdd(commandId, default);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsAuto(byte commandId) => AutoCommands.ContainsKey(commandId);
|
||||||
|
|
||||||
|
public static bool IsClick(byte commandId) => commandId is 0xF8 or 0xF5;
|
||||||
|
}
|
||||||
|
}
|
146
Apm/DataRow.cs
Normal file
146
Apm/DataRow.cs
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
using AnotherReplayReader.ReplayFile;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.Apm
|
||||||
|
{
|
||||||
|
internal class DataRow
|
||||||
|
{
|
||||||
|
public const string AverageApmRow = "APM(整局游戏)";
|
||||||
|
public const string PartialApmRow = "APM(当前时间段)";
|
||||||
|
|
||||||
|
public string Name { get; }
|
||||||
|
public bool IsDisabled { get; }
|
||||||
|
public DataValue Player1Value => _values[0];
|
||||||
|
public DataValue Player2Value => _values[1];
|
||||||
|
public DataValue Player3Value => _values[2];
|
||||||
|
public DataValue Player4Value => _values[3];
|
||||||
|
public DataValue Player5Value => _values[4];
|
||||||
|
public DataValue Player6Value => _values[5];
|
||||||
|
|
||||||
|
private readonly IReadOnlyList<DataValue> _values;
|
||||||
|
|
||||||
|
public DataRow(string name,
|
||||||
|
IEnumerable<string> values,
|
||||||
|
bool isDisabled = false)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
IsDisabled = isDisabled;
|
||||||
|
|
||||||
|
if (values.Count() < 6)
|
||||||
|
{
|
||||||
|
values = values.Concat(new string[6 - values.Count()]);
|
||||||
|
}
|
||||||
|
_values = values.Select(x => new DataValue(x)).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<DataRow> GetList(ApmPlotter plotter,
|
||||||
|
ApmPlotterFilterOptions options,
|
||||||
|
TimeSpan begin,
|
||||||
|
TimeSpan end,
|
||||||
|
out int apmRowIndex)
|
||||||
|
{
|
||||||
|
// these commands should appear in this order by default
|
||||||
|
var orderedCommands = new List<byte>();
|
||||||
|
orderedCommands.AddRange(
|
||||||
|
[
|
||||||
|
0xF5,
|
||||||
|
0xF8,
|
||||||
|
0x2A,
|
||||||
|
0xFA,
|
||||||
|
0xFB,
|
||||||
|
0x07,
|
||||||
|
0x08,
|
||||||
|
0x05,
|
||||||
|
0x06,
|
||||||
|
0x09,
|
||||||
|
0x00,
|
||||||
|
0x0A,
|
||||||
|
0x03,
|
||||||
|
0x04,
|
||||||
|
0x28,
|
||||||
|
0x29,
|
||||||
|
0x0D,
|
||||||
|
0x0E,
|
||||||
|
0x15,
|
||||||
|
0x14,
|
||||||
|
0x36,
|
||||||
|
0x16,
|
||||||
|
0x2C,
|
||||||
|
0x1A,
|
||||||
|
0x4E,
|
||||||
|
0xFE,
|
||||||
|
0xFF,
|
||||||
|
0x32,
|
||||||
|
0x2E,
|
||||||
|
0x2F,
|
||||||
|
0x4B,
|
||||||
|
0x4C,
|
||||||
|
0x02,
|
||||||
|
0x0C,
|
||||||
|
0x10,
|
||||||
|
]);
|
||||||
|
orderedCommands.AddRange(RA3Commands.AutoCommands);
|
||||||
|
orderedCommands.AddRange(RA3Commands.UnknownCommands);
|
||||||
|
|
||||||
|
var dataList = new List<DataRow>
|
||||||
|
{
|
||||||
|
new("ID", plotter.Players.Select(x => x.PlayerName))
|
||||||
|
};
|
||||||
|
var isPartial = begin > TimeSpan.Zero || end <= plotter.ReplayLength;
|
||||||
|
if (isPartial)
|
||||||
|
{
|
||||||
|
string GetStatus(Player player, int i)
|
||||||
|
{
|
||||||
|
if (player.IsComputer)
|
||||||
|
{
|
||||||
|
return "这是 AI";
|
||||||
|
}
|
||||||
|
if (begin < plotter.StricterPlayerLifes[i])
|
||||||
|
{
|
||||||
|
return "存活";
|
||||||
|
}
|
||||||
|
return begin < plotter.PlayerLifes[i]
|
||||||
|
? "变身天眼帝国,或双手离开键盘"
|
||||||
|
: "可能已离开房间";
|
||||||
|
}
|
||||||
|
dataList.Add(new("存活状态(推测)", plotter.Players.Select(GetStatus)));
|
||||||
|
}
|
||||||
|
apmRowIndex = dataList.Count;
|
||||||
|
// kill-death ratio
|
||||||
|
if (plotter.Replay.Footer?.TryGetKillDeathRatios() is { } kdRatios)
|
||||||
|
{
|
||||||
|
var texts = kdRatios
|
||||||
|
.Take(plotter.Players.Length)
|
||||||
|
.Select(x => $"{x:0.##}");
|
||||||
|
dataList.Add(new("击杀阵亡比(存疑)", texts));
|
||||||
|
}
|
||||||
|
// get commands
|
||||||
|
var commandCounts = plotter.GetCommandCounts(begin, end);
|
||||||
|
// add commands in the order specified by the list
|
||||||
|
foreach (var command in orderedCommands)
|
||||||
|
{
|
||||||
|
var counts = commandCounts.TryGetValue(command, out var stored)
|
||||||
|
? stored
|
||||||
|
: new int[plotter.Players.Length];
|
||||||
|
if (!isPartial || counts.Any(x => x > 0))
|
||||||
|
{
|
||||||
|
dataList.Add(new(RA3Commands.GetCommandName(command),
|
||||||
|
counts.Select(x => $"{x}"),
|
||||||
|
options.ShouldSkip(command)));
|
||||||
|
}
|
||||||
|
commandCounts.Remove(command);
|
||||||
|
}
|
||||||
|
// add other commands
|
||||||
|
foreach (var commandCount in commandCounts)
|
||||||
|
{
|
||||||
|
dataList.Add(new(RA3Commands.GetCommandName(commandCount.Key),
|
||||||
|
commandCount.Value.Select(x => $"{x}"),
|
||||||
|
options.ShouldSkip(commandCount.Key)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
43
Apm/DataValue.cs
Normal file
43
Apm/DataValue.cs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.Apm
|
||||||
|
{
|
||||||
|
internal class DataValue : IComparable<DataValue>, IComparable
|
||||||
|
{
|
||||||
|
public int? NumberValue { get; }
|
||||||
|
public string Value { get; }
|
||||||
|
|
||||||
|
public DataValue(string value)
|
||||||
|
{
|
||||||
|
Value = value;
|
||||||
|
if (int.TryParse(value, out var numberValue))
|
||||||
|
{
|
||||||
|
NumberValue = numberValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() => Value;
|
||||||
|
|
||||||
|
public int CompareTo(DataValue other)
|
||||||
|
{
|
||||||
|
if (NumberValue.HasValue == other.NumberValue.HasValue)
|
||||||
|
{
|
||||||
|
if (!NumberValue.HasValue)
|
||||||
|
{
|
||||||
|
return Value.CompareTo(other.Value);
|
||||||
|
}
|
||||||
|
return NumberValue.Value.CompareTo(other.NumberValue!.Value);
|
||||||
|
}
|
||||||
|
return NumberValue.HasValue ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CompareTo(object obj)
|
||||||
|
{
|
||||||
|
if (obj is DataValue other)
|
||||||
|
{
|
||||||
|
return CompareTo(other);
|
||||||
|
}
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
122
ApmWindow.xaml
Normal file
122
ApmWindow.xaml
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
<Window x:Class="AnotherReplayReader.ApmWindow"
|
||||||
|
x:ClassModifier="internal"
|
||||||
|
x:Name="ApmWindowInstance"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:local="clr-namespace:AnotherReplayReader"
|
||||||
|
xmlns:ox="http://oxyplot.org/wpf"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
Title="APM"
|
||||||
|
Height="550"
|
||||||
|
Width="800"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
Loaded="OnApmWindowLoaded">
|
||||||
|
<Grid Margin="0">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="200*" />
|
||||||
|
<RowDefinition Height="300*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<GridSplitter Grid.Row="0"
|
||||||
|
VerticalAlignment="Bottom"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Height="6"
|
||||||
|
BorderBrush="Gray"
|
||||||
|
BorderThickness="1" />
|
||||||
|
<DockPanel Grid.Row="0"
|
||||||
|
Margin="0,0,0,12">
|
||||||
|
<StackPanel DockPanel.Dock="Bottom"
|
||||||
|
Margin="16,0"
|
||||||
|
Orientation="Horizontal">
|
||||||
|
<Label Content="分辨率"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
<TextBox x:Name="_resolution"
|
||||||
|
Width="42"
|
||||||
|
TextAlignment="Right"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
PreviewTextInput="OnResolutionPreviewTextInput"
|
||||||
|
TextChanged="OnResolutionTextChanged" />
|
||||||
|
<Label Content="秒"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
<CheckBox x:Name="_skipUnknowns"
|
||||||
|
Margin="24,0,0,0"
|
||||||
|
Content="忽略未知操作"
|
||||||
|
IsChecked="True"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Checked="OnSkipUnknownsCheckedChanged"
|
||||||
|
Unchecked="OnSkipUnknownsCheckedChanged" />
|
||||||
|
<CheckBox x:Name="_skipAutos"
|
||||||
|
Margin="24,0,0,0"
|
||||||
|
Content="忽略自动操作"
|
||||||
|
IsChecked="True"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Checked="OnSkipAutosCheckedChanged"
|
||||||
|
Unchecked="OnSkipAutosCheckedChanged" />
|
||||||
|
<CheckBox x:Name="_skipClicks"
|
||||||
|
Margin="24,0,0,0"
|
||||||
|
Content="忽略左键点选"
|
||||||
|
IsChecked="False"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Checked="OnSkipClicksCheckedChanged"
|
||||||
|
Unchecked="OnSkipClicksCheckedChanged" />
|
||||||
|
</StackPanel>
|
||||||
|
<ox:PlotView x:Name="_plot"
|
||||||
|
DataContext="{Binding ElementName=ApmWindowInstance, Path=PlotModel}"
|
||||||
|
Model="{Binding Model}" />
|
||||||
|
</DockPanel>
|
||||||
|
<DockPanel Grid.Row="1"
|
||||||
|
Margin="16,8,16,16">
|
||||||
|
<DockPanel DockPanel.Dock="Top">
|
||||||
|
<Label x:Name="_label"
|
||||||
|
Grid.Column="1"
|
||||||
|
Margin="0"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
选择表格之后按 Ctrl+C 可以复制内容
|
||||||
|
</Label>
|
||||||
|
</DockPanel>
|
||||||
|
<DataGrid x:Name="_table"
|
||||||
|
CanUserSortColumns="True"
|
||||||
|
VirtualizingPanel.ScrollUnit="Pixel">
|
||||||
|
<DataGrid.Resources>
|
||||||
|
<Style TargetType="DataGridCell">
|
||||||
|
<EventSetter Event="MouseDoubleClick"
|
||||||
|
Handler="OnTableMouseDoubleClick" />
|
||||||
|
</Style>
|
||||||
|
<Style TargetType="DataGridRow">
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding Path=IsDisabled}"
|
||||||
|
Value="True">
|
||||||
|
<Setter Property="Foreground"
|
||||||
|
Value="Gray" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</DataGrid.Resources>
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTextColumn Header="项"
|
||||||
|
Binding="{Binding Path=Name}"
|
||||||
|
IsReadOnly="True" />
|
||||||
|
<DataGridTextColumn Header="玩家1"
|
||||||
|
Binding="{Binding Path=Player1Value}"
|
||||||
|
IsReadOnly="True" />
|
||||||
|
<DataGridTextColumn Header="玩家2"
|
||||||
|
Binding="{Binding Path=Player2Value}"
|
||||||
|
IsReadOnly="True" />
|
||||||
|
<DataGridTextColumn Header="玩家3"
|
||||||
|
Binding="{Binding Path=Player3Value}"
|
||||||
|
IsReadOnly="True" />
|
||||||
|
<DataGridTextColumn Header="玩家4"
|
||||||
|
Binding="{Binding Path=Player4Value}"
|
||||||
|
IsReadOnly="True" />
|
||||||
|
<DataGridTextColumn Header="玩家5"
|
||||||
|
Binding="{Binding Path=Player5Value}"
|
||||||
|
IsReadOnly="True" />
|
||||||
|
<DataGridTextColumn Header="玩家6"
|
||||||
|
Binding="{Binding Path=Player6Value}"
|
||||||
|
IsReadOnly="True" />
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
</DockPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
387
ApmWindow.xaml.cs
Normal file
387
ApmWindow.xaml.cs
Normal file
@ -0,0 +1,387 @@
|
|||||||
|
using AnotherReplayReader.Apm;
|
||||||
|
using AnotherReplayReader.ReplayFile;
|
||||||
|
using AnotherReplayReader.Utils;
|
||||||
|
using OxyPlot;
|
||||||
|
using OxyPlot.Annotations;
|
||||||
|
using OxyPlot.Axes;
|
||||||
|
using OxyPlot.Legends;
|
||||||
|
using OxyPlot.Series;
|
||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Input;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// APM.xaml 的交互逻辑
|
||||||
|
/// </summary>
|
||||||
|
internal partial class ApmWindow : Window
|
||||||
|
{
|
||||||
|
public static readonly TimeSpan DefaultResolution = TimeSpan.FromSeconds(15);
|
||||||
|
private readonly Regex _resolutionRegex = new(@"[^0-9]+");
|
||||||
|
private readonly Replay _replay;
|
||||||
|
private readonly Task<ApmPlotter> _plotter;
|
||||||
|
private readonly ApmWindowPlotController _plotController;
|
||||||
|
|
||||||
|
public ApmWindowPlotViewModel PlotModel { get; } = new();
|
||||||
|
public TimeSpan PlotResolution { get; private set; } = DefaultResolution;
|
||||||
|
public bool SkipUnknowns { get; private set; } = true;
|
||||||
|
public bool SkipAutos { get; private set; } = true;
|
||||||
|
public bool SkipClicks { get; private set; } = false;
|
||||||
|
|
||||||
|
public ApmWindow(Replay replay)
|
||||||
|
{
|
||||||
|
_replay = replay;
|
||||||
|
_plotter = Task.Run(() => new ApmPlotter(replay));
|
||||||
|
_plotController = new(this);
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void FilterData(TimeSpan begin, TimeSpan end, bool updatePlot)
|
||||||
|
{
|
||||||
|
await FilterDataAsync(begin, end, updatePlot);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnApmWindowLoaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_plot.Controller = _plotController;
|
||||||
|
_resolution.Text = PlotResolution.TotalSeconds.ToString();
|
||||||
|
_skipUnknowns.IsChecked = SkipUnknowns;
|
||||||
|
_skipAutos.IsChecked = SkipAutos;
|
||||||
|
_skipClicks.IsChecked = SkipClicks;
|
||||||
|
await InitializeApmWindowData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InitializeApmWindowData()
|
||||||
|
{
|
||||||
|
await FilterDataAsync(TimeSpan.MinValue, TimeSpan.MaxValue, true);
|
||||||
|
_label.Content = "选择表格之后按 Ctrl+C 可以复制内容";
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task FilterDataAsync(TimeSpan begin, TimeSpan end, bool updatePlot)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await FilterDataAsyncThrowable(begin, end, updatePlot);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, $"加载录像信息失败:\r\n{e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task FilterDataAsyncThrowable(TimeSpan begin, TimeSpan end, bool updatePlot)
|
||||||
|
{
|
||||||
|
var resolution = PlotResolution;
|
||||||
|
var options = new ApmPlotterFilterOptions(!SkipUnknowns, !SkipAutos, !SkipClicks);
|
||||||
|
var (dataList, plotData, replayLength, isPartial) = await Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var plotter = await _plotter.ConfigureAwait(false);
|
||||||
|
var list = DataRow.GetList(plotter, options, begin, end, out var apmIndex);
|
||||||
|
// avg apm
|
||||||
|
var avg = plotter.CalculateAverageApm(options);
|
||||||
|
// data for plotting and partial apm
|
||||||
|
var data = plotter.GetPoints(resolution, options);
|
||||||
|
// partial apm
|
||||||
|
var isPartial = begin > TimeSpan.Zero || end <= plotter.ReplayLength;
|
||||||
|
if (isPartial)
|
||||||
|
{
|
||||||
|
var instantApms = ApmPlotter.CalculateInstantApm(data, begin, end);
|
||||||
|
string PartialApmToString(double v, int i)
|
||||||
|
{
|
||||||
|
if (plotter.Players[i].IsComputer)
|
||||||
|
{
|
||||||
|
return "这是 AI";
|
||||||
|
}
|
||||||
|
return plotter.PlayerLifes[i] < begin
|
||||||
|
? "玩家已战败"
|
||||||
|
: $"{v:0.##}";
|
||||||
|
}
|
||||||
|
list.Insert(apmIndex, new(DataRow.PartialApmRow,
|
||||||
|
instantApms.Select(PartialApmToString)));
|
||||||
|
}
|
||||||
|
list.Insert(apmIndex, new(DataRow.AverageApmRow,
|
||||||
|
avg.Select(v => $"{v:0.##}")));
|
||||||
|
|
||||||
|
return (list, data, plotter.ReplayLength, isPartial);
|
||||||
|
});
|
||||||
|
if (updatePlot)
|
||||||
|
{
|
||||||
|
BuildPlot(plotData, replayLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
_table.Items.Clear();
|
||||||
|
foreach (var row in dataList)
|
||||||
|
{
|
||||||
|
_table.Items.Add(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
_table.Columns[1].Header = dataList[0].Player1Value;
|
||||||
|
_table.Columns[2].Header = dataList[0].Player2Value;
|
||||||
|
_table.Columns[3].Header = dataList[0].Player3Value;
|
||||||
|
_table.Columns[4].Header = dataList[0].Player4Value;
|
||||||
|
_table.Columns[5].Header = dataList[0].Player5Value;
|
||||||
|
_table.Columns[6].Header = dataList[0].Player6Value;
|
||||||
|
if (isPartial)
|
||||||
|
{
|
||||||
|
_label.Content = $"{(ShortTimeSpan)begin} 至 {(ShortTimeSpan)end} 之间的 APM 数据";
|
||||||
|
_label.FontWeight = System.Windows.FontWeights.Bold;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_label.Content = "以下是整局游戏的 APM 数据:";
|
||||||
|
_label.FontWeight = System.Windows.FontWeights.Normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildPlot(DataPoint[][] data, TimeSpan replayLength)
|
||||||
|
{
|
||||||
|
var model = new PlotModel();
|
||||||
|
model.Annotations.Add(new ApmWindowPlotTextAnnotation
|
||||||
|
{
|
||||||
|
Text = "鼠标左键拖拽选择,滚轮缩放,右键拖拽移动",
|
||||||
|
});
|
||||||
|
var maxApm = data.SelectMany(x => x).Max(x => x.Y);
|
||||||
|
var yaxis = new LinearAxis
|
||||||
|
{
|
||||||
|
Position = AxisPosition.Right,
|
||||||
|
IsZoomEnabled = false,
|
||||||
|
AbsoluteMinimum = -maxApm / 10,
|
||||||
|
AbsoluteMaximum = maxApm,
|
||||||
|
MajorStep = 50,
|
||||||
|
};
|
||||||
|
var lengthInSeconds = replayLength.TotalSeconds;
|
||||||
|
var limit = lengthInSeconds / 10;
|
||||||
|
var xaxis = new TimeSpanAxis
|
||||||
|
{
|
||||||
|
Position = AxisPosition.Bottom,
|
||||||
|
AbsoluteMinimum = -limit,
|
||||||
|
AbsoluteMaximum = lengthInSeconds + limit,
|
||||||
|
};
|
||||||
|
model.Axes.Add(yaxis);
|
||||||
|
model.Axes.Add(xaxis);
|
||||||
|
foreach (var (points, i) in data.Select((v, i) => (v, i)))
|
||||||
|
{
|
||||||
|
var series = new LineSeries
|
||||||
|
{
|
||||||
|
Title = _replay.Players[i].PlayerName,
|
||||||
|
};
|
||||||
|
series.Points.AddRange(points);
|
||||||
|
model.Series.Add(series);
|
||||||
|
model.Legends.Add(new Legend());
|
||||||
|
}
|
||||||
|
PlotModel.Model = model;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTableMouseDoubleClick(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
_table.SelectAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int NormalizeResolutionInput(string text)
|
||||||
|
{
|
||||||
|
if (!int.TryParse(text, out var value))
|
||||||
|
{
|
||||||
|
value = (int)PlotResolution.TotalSeconds;
|
||||||
|
}
|
||||||
|
return Math.Min(Math.Max(1, value), 3600);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnResolutionPreviewTextInput(object sender, TextCompositionEventArgs e)
|
||||||
|
{
|
||||||
|
e.Handled = _resolutionRegex.IsMatch(e.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnResolutionTextChanged(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
var seconds = NormalizeResolutionInput(_resolution.Text);
|
||||||
|
if (Math.Abs(seconds - PlotResolution.TotalSeconds) >= 0.5)
|
||||||
|
{
|
||||||
|
PlotResolution = TimeSpan.FromSeconds(seconds);
|
||||||
|
_resolution.Text = seconds.ToString();
|
||||||
|
_plotController.DiscardRectangle();
|
||||||
|
await FilterDataAsync(TimeSpan.MinValue, TimeSpan.MaxValue, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnSkipUnknownsCheckedChanged(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_skipUnknowns.IsChecked != SkipUnknowns)
|
||||||
|
{
|
||||||
|
SkipUnknowns = _skipUnknowns.IsChecked is true;
|
||||||
|
_plotController.DiscardRectangle();
|
||||||
|
await FilterDataAsync(TimeSpan.MinValue, TimeSpan.MaxValue, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnSkipAutosCheckedChanged(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_skipAutos.IsChecked != SkipAutos)
|
||||||
|
{
|
||||||
|
SkipAutos = _skipAutos.IsChecked is true;
|
||||||
|
_plotController.DiscardRectangle();
|
||||||
|
await FilterDataAsync(TimeSpan.MinValue, TimeSpan.MaxValue, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnSkipClicksCheckedChanged(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_skipClicks.IsChecked != SkipClicks)
|
||||||
|
{
|
||||||
|
SkipClicks = _skipClicks.IsChecked is true;
|
||||||
|
_plotController.DiscardRectangle();
|
||||||
|
await FilterDataAsync(TimeSpan.MinValue, TimeSpan.MaxValue, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class ApmWindowPlotViewModel : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
private PlotModel _model = new();
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
public PlotModel Model
|
||||||
|
{
|
||||||
|
get => _model;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_model = value;
|
||||||
|
PropertyChanged?.Invoke(this, new(nameof(Model)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class ApmWindowPlotController : PlotController
|
||||||
|
{
|
||||||
|
private readonly ApmWindow _window;
|
||||||
|
private RectangleAnnotation? _current;
|
||||||
|
private PlotModel Plot => _window.PlotModel.Model;
|
||||||
|
private double Resolution => _window.PlotResolution.TotalSeconds;
|
||||||
|
// private readonly Func<OxyMouseDownEventArgs>
|
||||||
|
|
||||||
|
public ApmWindowPlotController(ApmWindow window)
|
||||||
|
{
|
||||||
|
_window = window;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DiscardRectangle()
|
||||||
|
{
|
||||||
|
_current = null;
|
||||||
|
RemoveSelections();
|
||||||
|
Plot.InvalidatePlot(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool HandleMouseDown(IView view, OxyMouseDownEventArgs args)
|
||||||
|
{
|
||||||
|
TryBeginDragRectagle(args);
|
||||||
|
return base.HandleMouseDown(view, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool HandleMouseMove(IView view, OxyMouseEventArgs args)
|
||||||
|
{
|
||||||
|
TryDragRectangle(args);
|
||||||
|
return base.HandleMouseMove(view, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool HandleMouseUp(IView view, OxyMouseEventArgs args)
|
||||||
|
{
|
||||||
|
TryEndDragRectangle();
|
||||||
|
return base.HandleMouseUp(view, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveSelections()
|
||||||
|
{
|
||||||
|
var list = Plot.Annotations
|
||||||
|
.Select((x, i) => (Item: x, Index: i))
|
||||||
|
.Where(t => t.Item is RectangleAnnotation)
|
||||||
|
.Reverse()
|
||||||
|
.ToArray();
|
||||||
|
foreach (var (_, index) in list)
|
||||||
|
{
|
||||||
|
Plot.Annotations.RemoveAt(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryBeginDragRectagle(OxyMouseDownEventArgs args)
|
||||||
|
{
|
||||||
|
if (args.ChangedButton == OxyMouseButton.Left)
|
||||||
|
{
|
||||||
|
var x = Plot.Axes[1].InverseTransform(args.Position.X);
|
||||||
|
x = Math.Round(x / Resolution) * Resolution;
|
||||||
|
RemoveSelections();
|
||||||
|
_current = new()
|
||||||
|
{
|
||||||
|
ClipByYAxis = true,
|
||||||
|
Fill = OxyColor.FromArgb(128, 0, 128, 255),
|
||||||
|
MinimumY = double.NegativeInfinity,
|
||||||
|
MaximumY = double.PositiveInfinity,
|
||||||
|
MinimumX = x,
|
||||||
|
MaximumX = x
|
||||||
|
};
|
||||||
|
Plot.Annotations.Add(_current);
|
||||||
|
Plot.InvalidatePlot(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryDragRectangle(OxyMouseEventArgs args)
|
||||||
|
{
|
||||||
|
if (_current is not null)
|
||||||
|
{
|
||||||
|
var x = Plot.Axes[1].InverseTransform(args.Position.X);
|
||||||
|
var offsetMultiplier = Math.Round((x - _current.MinimumX) / Resolution);
|
||||||
|
x = offsetMultiplier * Resolution;
|
||||||
|
_current.MaximumX = _current.MinimumX + x;
|
||||||
|
if (_current.MaximumX < _current.MinimumX)
|
||||||
|
{
|
||||||
|
var temp = _current.MinimumX;
|
||||||
|
_current.MinimumX = _current.MaximumX;
|
||||||
|
_current.MaximumX = temp;
|
||||||
|
}
|
||||||
|
Plot.InvalidatePlot(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryEndDragRectangle()
|
||||||
|
{
|
||||||
|
if (_current is not null)
|
||||||
|
{
|
||||||
|
if (Math.Abs(_current.MaximumX - _current.MinimumX) < Resolution)
|
||||||
|
{
|
||||||
|
RemoveSelections();
|
||||||
|
Plot.InvalidatePlot(false);
|
||||||
|
_window.FilterData(TimeSpan.MinValue, TimeSpan.MaxValue, false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_window.FilterData(TimeSpan.FromSeconds(_current.MinimumX),
|
||||||
|
TimeSpan.FromSeconds(_current.MaximumX),
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
_current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ApmWindowPlotTextAnnotation : Annotation
|
||||||
|
{
|
||||||
|
public string Text { get; set; } = string.Empty;
|
||||||
|
public double X { get; set; } = 8;
|
||||||
|
public double Y { get; set; } = 8;
|
||||||
|
|
||||||
|
public override void Render(IRenderContext rc)
|
||||||
|
{
|
||||||
|
base.Render(rc);
|
||||||
|
double pX = PlotModel.PlotArea.Left + X;
|
||||||
|
double pY = PlotModel.PlotArea.Top + Y;
|
||||||
|
rc.DrawMultilineText(new(pX, pY),
|
||||||
|
Text,
|
||||||
|
PlotModel.TextColor,
|
||||||
|
PlotModel.DefaultFont,
|
||||||
|
PlotModel.DefaultFontSize,
|
||||||
|
PlotModel.SubtitleFontWeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
App.xaml
3
App.xaml
@ -2,7 +2,8 @@
|
|||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:local="clr-namespace:AnotherReplayReader"
|
xmlns:local="clr-namespace:AnotherReplayReader"
|
||||||
StartupUri="MainWindow.xaml">
|
StartupUri="MainWindow.xaml"
|
||||||
|
ShutdownMode="OnMainWindowClose">
|
||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
|
|
||||||
</Application.Resources>
|
</Application.Resources>
|
||||||
|
67
App.xaml.cs
67
App.xaml.cs
@ -1,8 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.IO;
|
||||||
using System.Configuration;
|
using System.Reflection;
|
||||||
using System.Data;
|
using System.Runtime.InteropServices;
|
||||||
using System.Linq;
|
using System.Threading;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
namespace AnotherReplayReader
|
||||||
@ -12,5 +12,64 @@ namespace AnotherReplayReader
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class App : Application
|
public partial class App : Application
|
||||||
{
|
{
|
||||||
|
private int _isInException = 0;
|
||||||
|
|
||||||
|
public const string Version = "0.7";
|
||||||
|
public const string Name = "自动录像机";
|
||||||
|
public const string NameWithVersion = Name + " v" + Version;
|
||||||
|
|
||||||
|
private static readonly Lazy<string> _libsFolder = new(GetLibraryFolder, LazyThreadSafetyMode.PublicationOnly);
|
||||||
|
public static string LibsFolder => _libsFolder.Value;
|
||||||
|
|
||||||
|
public App()
|
||||||
|
{
|
||||||
|
static Assembly? LoadFromLibsFolder(object sender, ResolveEventArgs args)
|
||||||
|
{
|
||||||
|
var assemblyPath = Path.Combine(LibsFolder, new AssemblyName(args.Name).Name + ".dll");
|
||||||
|
return File.Exists(assemblyPath)
|
||||||
|
? Assembly.LoadFrom(assemblyPath)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(LoadFromLibsFolder);
|
||||||
|
|
||||||
|
AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) =>
|
||||||
|
{
|
||||||
|
if (Interlocked.Increment(ref _isInException) > 1)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
const string message = "哎呀呀,出现了一些无法处理的问题,只能退出了。要不要尝试保存一下日志文件呢?";
|
||||||
|
var choice = MessageBox.Show($"{message}\r\n{eventArgs.ExceptionObject}", Name, MessageBoxButton.YesNo);
|
||||||
|
if (choice == MessageBoxResult.Yes)
|
||||||
|
{
|
||||||
|
Debug.Instance.RequestSave();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetLibraryFolder() => Path.Combine(GetExecutableFolder(), nameof(AnotherReplayReader) + "Data");
|
||||||
|
|
||||||
|
private static string GetExecutableFolder()
|
||||||
|
{
|
||||||
|
char[]? buffer = null;
|
||||||
|
uint result;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
buffer = new char[(buffer?.Length ?? 128) * 2];
|
||||||
|
result = GetModuleFileNameW(IntPtr.Zero, buffer, buffer.Length);
|
||||||
|
if (result is 0)
|
||||||
|
{
|
||||||
|
throw new Exception("Failed to retrieve executable name");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (result >= buffer.Length);
|
||||||
|
return Path.GetDirectoryName(new(buffer, 0, Array.IndexOf(buffer, '\0')));
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("Kernel32.dll", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
|
||||||
|
private static extern uint GetModuleFileNameW(IntPtr module, char[] fileName, int size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
93
Auth.cs
93
Auth.cs
@ -1,93 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using Microsoft.Win32;
|
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
|
||||||
{
|
|
||||||
internal static class Auth
|
|
||||||
{
|
|
||||||
public static string ID { get; private set; }
|
|
||||||
|
|
||||||
static Auth()
|
|
||||||
{
|
|
||||||
ID = null;
|
|
||||||
|
|
||||||
var windowsID = null as string;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var view64 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64))
|
|
||||||
using (var winNt = view64?.OpenSubKey(@"Software\Microsoft\Windows NT\CurrentVersion", false))
|
|
||||||
{
|
|
||||||
windowsID = winNt?.GetValue("ProductId") as string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(Exception)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var randomKey = null as string;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var folderPath = Cache.CacheDirectory;
|
|
||||||
if (!Directory.Exists(folderPath))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(folderPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
var keyPath = Path.Combine(folderPath, "id");
|
|
||||||
if(!File.Exists(keyPath))
|
|
||||||
{
|
|
||||||
File.WriteAllText(keyPath, Guid.NewGuid().ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
randomKey = File.ReadAllText(keyPath);
|
|
||||||
}
|
|
||||||
catch(Exception)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(string.IsNullOrWhiteSpace(windowsID) || string.IsNullOrWhiteSpace(randomKey))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
using (var sha = SHA256.Create())
|
|
||||||
{
|
|
||||||
var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(windowsID + randomKey));
|
|
||||||
ID = string.Concat(hash.Skip(3).Take(10).Select(x => $"{x:X2}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string GetKey()
|
|
||||||
{
|
|
||||||
if(ID == null)
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
var text = $"{ID}{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}";
|
|
||||||
var bytes = Encoding.UTF8.GetBytes(text);
|
|
||||||
var pre = Encoding.UTF8.GetBytes("playertable!");
|
|
||||||
var salt = new byte[9];
|
|
||||||
using (var rng = new RNGCryptoServiceProvider())
|
|
||||||
{
|
|
||||||
rng.GetNonZeroBytes(salt);
|
|
||||||
}
|
|
||||||
|
|
||||||
for(var i = 0; i < bytes.Length; ++i)
|
|
||||||
{
|
|
||||||
bytes[i] = (byte)(bytes[i] ^ salt[i % salt.Length]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Convert.ToBase64String(salt.Concat(bytes).Select((x, i) => (byte)(x ^ pre[i % pre.Length])).ToArray());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,172 +1,36 @@
|
|||||||
using System;
|
using AnotherReplayReader.Utils;
|
||||||
using System.Collections.Generic;
|
using Microsoft.Win32;
|
||||||
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Reflection;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Win32;
|
using TechnologyAssembler.Core.IO;
|
||||||
using OpenSage.FileFormats.Big;
|
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
namespace AnotherReplayReader
|
||||||
{
|
{
|
||||||
internal sealed class BigMinimapCache
|
internal sealed class BigMinimapCache
|
||||||
{
|
{
|
||||||
private sealed class CacheAdapter
|
private readonly object _lock = new();
|
||||||
{
|
private SkuDefFileSystemProvider? _skudefFileSystem = null;
|
||||||
public List<string> Bigs { get; set; }
|
|
||||||
public Dictionary<string, string> MapsToBigs { get; set; }
|
|
||||||
|
|
||||||
public CacheAdapter()
|
public BigMinimapCache(string? ra3Directory)
|
||||||
{
|
{
|
||||||
Bigs = new List<string>();
|
Task.Run(() => Initialize(ra3Directory));
|
||||||
MapsToBigs = new Dictionary<string, string>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//private Cache _cache;
|
public bool TryGetEntry(string path, out Stream? bigEntry)
|
||||||
private volatile IReadOnlyDictionary<string, string> _mapsToBigs = null;
|
|
||||||
|
|
||||||
public BigMinimapCache(Cache cache, string ra3Directory)
|
|
||||||
{
|
{
|
||||||
//_cache = cache;
|
bigEntry = null;
|
||||||
|
|
||||||
Task.Run(() =>
|
using var locker = new Lock(_lock);
|
||||||
{
|
if (_skudefFileSystem is not { } fs)
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!Directory.Exists(ra3Directory))
|
|
||||||
{
|
|
||||||
Debug.Instance.DebugMessage += $"Will not initialize BigMinimapCache because RA3Directory {ra3Directory} does not exist.\r\n";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var bigSet = ParseSkudefs(Directory.EnumerateFiles(ra3Directory, "*.SkuDef"));
|
|
||||||
|
|
||||||
//var cached = _cache.GetOrDefault("bigsCache", new CacheAdapter());
|
|
||||||
var mapsToBigs = new Dictionary<string, string>();
|
|
||||||
|
|
||||||
foreach (var bigPath in bigSet/*.Where(x => !cached.Bigs.Contains(x))*/)
|
|
||||||
{
|
|
||||||
if (!File.Exists(bigPath))
|
|
||||||
{
|
|
||||||
Debug.Instance.DebugMessage += $"Big {bigPath} does not exist.\r\n";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Debug.Instance.DebugMessage += $"Trying to add Big {bigPath} to big minimap cache...\r\n";
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var big = new BigArchive(bigPath))
|
|
||||||
{
|
|
||||||
foreach (var entry in big.Entries)
|
|
||||||
{
|
|
||||||
if (entry.FullName.EndsWith("_art.tga", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
mapsToBigs[entry.FullName] = bigPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(Exception exception)
|
|
||||||
{
|
|
||||||
Debug.Instance.DebugMessage += $"Exception when reading big:\r\n {exception}\r\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//cached.Bigs = bigSet.ToList();
|
|
||||||
|
|
||||||
//_cache.Set("bigsCache", cached);
|
|
||||||
_mapsToBigs = mapsToBigs; //cached.MapsToBigs;
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
Debug.Instance.DebugMessage += $"Exception during initialization of BigMinimapCache: \r\n{exception}\r\n";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static HashSet<string> ParseSkudefs(IEnumerable<string> skudefs)
|
|
||||||
{
|
|
||||||
var skudefSet = new HashSet<string>(skudefs.Select(x => x.ToLowerInvariant()));
|
|
||||||
var unreadSkudefs = new HashSet<string>();
|
|
||||||
var bigSet = new HashSet<string>();
|
|
||||||
|
|
||||||
void ReadSkudefLine(string baseDirectory, string line, string expectedCommand, Action<string> action)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
char[] separators = { ' ', '\t' };
|
|
||||||
line = line.ToLowerInvariant();
|
|
||||||
var splitted = line.Split(separators, 2, StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
if (splitted[0].Equals(expectedCommand))
|
|
||||||
{
|
|
||||||
var path = splitted[1];
|
|
||||||
if (!Path.IsPathRooted(path))
|
|
||||||
{
|
|
||||||
path = Path.Combine(baseDirectory, path);
|
|
||||||
}
|
|
||||||
action(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
Debug.Instance.DebugMessage += $"Exception when parsing skudef line:\r\n {exception}\r\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ReadSkudef(string fileName, Action<string, string> onBaseDirectoryAndLine)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var baseDirectory = Path.GetDirectoryName(fileName).ToLowerInvariant();
|
|
||||||
foreach (var line in File.ReadAllLines(fileName))
|
|
||||||
{
|
|
||||||
onBaseDirectoryAndLine(baseDirectory, line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
Debug.Instance.DebugMessage += $"Exception when parsing skudef file:\r\n {exception}\r\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var skudef in skudefSet)
|
|
||||||
{
|
|
||||||
ReadSkudef(skudef, (baseDirectory, line) =>
|
|
||||||
{
|
|
||||||
ReadSkudefLine(baseDirectory, line, "add-config", x =>
|
|
||||||
{
|
|
||||||
if (!skudefSet.Contains(x))
|
|
||||||
{
|
|
||||||
unreadSkudefs.Add(x);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ReadSkudefLine(baseDirectory, line, "add-big", x => bigSet.Add(x));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var skudef in unreadSkudefs)
|
|
||||||
{
|
|
||||||
ReadSkudef(skudef, (baseDirectory, line) =>
|
|
||||||
{
|
|
||||||
ReadSkudefLine(baseDirectory, line, "add-big", x => bigSet.Add(x));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return bigSet;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryGetBigByEntryPath(string path, out BigArchive big)
|
|
||||||
{
|
|
||||||
big = null;
|
|
||||||
|
|
||||||
if (_mapsToBigs == null)
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_mapsToBigs.ContainsKey(path))
|
if (!fs.FileExists(path))
|
||||||
{
|
{
|
||||||
Debug.Instance.DebugMessage += $"Cannot find big entry [{path}].\r\n";
|
Debug.Instance.DebugMessage += $"Cannot find big entry [{path}].\r\n";
|
||||||
return false;
|
return false;
|
||||||
@ -174,58 +38,83 @@ namespace AnotherReplayReader
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var bigPath = _mapsToBigs[path];
|
bigEntry = fs.OpenStream(path, VirtualFileModeType.Open);
|
||||||
big = new BigArchive(bigPath);
|
return true;
|
||||||
if(big.GetEntry(path) == null)
|
|
||||||
{
|
|
||||||
//_cache.Remove("bigsCache");
|
|
||||||
big.Dispose();
|
|
||||||
big = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
Debug.Instance.DebugMessage += $"Exception during query (entryStream) of BigMinimapCache: \r\n{exception}\r\n";
|
Debug.Instance.DebugMessage += $"Exception during query (entryStream) of BigMinimapCache: \r\n{exception}\r\n";
|
||||||
big = null;
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 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
|
try
|
||||||
{
|
{
|
||||||
var bigPath = _mapsToBigs[path];
|
if (ra3Directory is null || !Directory.Exists(ra3Directory))
|
||||||
using (var big = new BigArchive(path))
|
|
||||||
{
|
{
|
||||||
var entry = big.GetEntry(path);
|
Debug.Instance.DebugMessage += $"Will not initialize BigMinimapCache because RA3Directory {ra3Directory} does not exist.\r\n";
|
||||||
using (var stream = entry.Open())
|
return;
|
||||||
using (var reader = new BinaryReader(stream))
|
}
|
||||||
|
|
||||||
|
var currentLanguage = RegistryUtils.RetrieveInRa3(RegistryHive.CurrentUser, "Language");
|
||||||
|
var currentLanguage_ = $"{currentLanguage}_";
|
||||||
|
double SkudefVersionSelector(string fullPath)
|
||||||
|
{
|
||||||
|
var value = -1.0;
|
||||||
|
try
|
||||||
{
|
{
|
||||||
return reader.ReadBytes((int)entry.Length);
|
const string skudefPrefix = "RA3";
|
||||||
|
const int prefix = 1;
|
||||||
|
const int language = 2;
|
||||||
|
const int majorVersion = 3;
|
||||||
|
const int minorVersion = 4;
|
||||||
|
const int majorVersionMultiplier = 10000;
|
||||||
|
const int correctLanguageBonus = 1000_0000;
|
||||||
|
|
||||||
|
value = 0;
|
||||||
|
var stem = Path.GetFileNameWithoutExtension(fullPath);
|
||||||
|
var match = Regex.Match(stem, @"([^_]*)_([^0-9]*)([0-9]*)\.([0-9]*)");
|
||||||
|
if (!match.Success || match.Groups.Cast<Group>().Any(g => !g.Success))
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
value += match.Groups[prefix].Value == skudefPrefix ? 0.1 : 0;
|
||||||
|
value += match.Groups[language].Value == currentLanguage_ ? correctLanguageBonus : 0;
|
||||||
|
value += int.Parse(match.Groups[majorVersion].Value) * majorVersionMultiplier;
|
||||||
|
value += int.Parse(match.Groups[minorVersion].Value);
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Debug.Instance.DebugMessage += $"Failed to retrieve skudef: {e}\r\n";
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
var highestSkudef = (from p in Directory.EnumerateFiles(ra3Directory, "*.SkuDef")
|
||||||
|
orderby SkudefVersionSelector(p) descending
|
||||||
|
select p).First();
|
||||||
|
Debug.Instance.DebugMessage += $"Retrieved highest skudef: {highestSkudef}\r\n";
|
||||||
|
|
||||||
|
using var locker = new Lock(_lock);
|
||||||
|
DronePlatform.BuildTechnologyAssembler();
|
||||||
|
_skudefFileSystem = new SkuDefFileSystemProvider("config", highestSkudef);
|
||||||
|
}
|
||||||
|
catch (ReflectionTypeLoadException e)
|
||||||
|
{
|
||||||
|
Debug.Instance.DebugMessage += $"Exception during initialization of BigMinimapCache: \r\n{e}\r\nLoader Exceptions: {e.LoaderExceptions.Length}";
|
||||||
|
foreach (var e2 in e.LoaderExceptions)
|
||||||
|
{
|
||||||
|
Debug.Instance.DebugMessage += $"Exception during initialization of BigMinimapCache: \r\n{e2}\r\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
Debug.Instance.DebugMessage += $"Exception during query (bytes) of BigMinimapCache: \r\n{exception}\r\n";
|
Debug.Instance.DebugMessage += $"Exception during initialization of BigMinimapCache: \r\n{exception}\r\n";
|
||||||
//_cache.Remove("bigsCache");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
95
Cache.cs
95
Cache.cs
@ -3,59 +3,99 @@ using System.Collections.Concurrent;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using System.Web.Script.Serialization;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using static System.Text.Json.JsonSerializer;
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
namespace AnotherReplayReader
|
||||||
{
|
{
|
||||||
internal sealed class Cache
|
public sealed class Cache
|
||||||
{
|
{
|
||||||
public static string CacheDirectory => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "RA3Bar.Lanyi.AnotherReplayReader");
|
public static string OldCacheDirectory => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "RA3Bar.Lanyi.AnotherReplayReader");
|
||||||
|
public static string CacheDirectory => App.LibsFolder;
|
||||||
public static string CacheFilePath => Path.Combine(CacheDirectory, "AnotherReplayReader.cache");
|
public static string CacheFilePath => Path.Combine(CacheDirectory, "AnotherReplayReader.cache");
|
||||||
|
|
||||||
private ConcurrentDictionary<string, string> _storage;
|
private readonly ConcurrentDictionary<string, string> _storage = new();
|
||||||
|
|
||||||
|
public Task Initialization { get; }
|
||||||
|
|
||||||
public Cache()
|
public Cache()
|
||||||
{
|
{
|
||||||
try
|
Initialization = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
if (!Directory.Exists(CacheDirectory))
|
try
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(CacheDirectory);
|
if (!Directory.Exists(CacheDirectory))
|
||||||
}
|
{
|
||||||
|
Directory.CreateDirectory(CacheDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
var serializer = new JavaScriptSerializer();
|
try
|
||||||
_storage = serializer.Deserialize<ConcurrentDictionary<string, string>>(File.ReadAllText(CacheFilePath));
|
{
|
||||||
}
|
var old = new DirectoryInfo(OldCacheDirectory);
|
||||||
catch
|
if (old.Exists)
|
||||||
{
|
{
|
||||||
_storage = new ConcurrentDictionary<string, string>();
|
foreach (var file in old.EnumerateFiles())
|
||||||
}
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
file.MoveTo(Path.Combine(CacheDirectory, file.Name));
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
old.Delete();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
using var cacheStream = File.OpenRead(CacheFilePath);
|
||||||
|
var futureCache = DeserializeAsync<Dictionary<string, string>>(cacheStream).ConfigureAwait(false);
|
||||||
|
if (await futureCache is not { } cached)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
foreach (var kv in cached)
|
||||||
|
{
|
||||||
|
_storage.TryAdd(kv.Key, kv.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public T GetOrDefault<T>(string key, in T defaultValue)
|
public T GetOrDefault<T>(string key, T defaultValue)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_storage.TryGetValue(key, out var valueString))
|
if (_storage.TryGetValue(key, out var valueString))
|
||||||
{
|
{
|
||||||
var serializer = new JavaScriptSerializer();
|
return Deserialize<T>(valueString) ?? defaultValue;
|
||||||
return serializer.Deserialize<T>(valueString);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Set<T>(string key, in T value)
|
public void Set<T>(string key, T value)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var serializer = new JavaScriptSerializer();
|
_storage[key] = Serialize(value);
|
||||||
_storage[key] = serializer.Serialize(value);
|
}
|
||||||
Save();
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetValues(params (string Key, object Value)[] values)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var (key, value) in values)
|
||||||
|
{
|
||||||
|
_storage[key] = Serialize(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
@ -63,15 +103,14 @@ namespace AnotherReplayReader
|
|||||||
public void Remove(string key)
|
public void Remove(string key)
|
||||||
{
|
{
|
||||||
_storage.TryRemove(key, out _);
|
_storage.TryRemove(key, out _);
|
||||||
Save();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Save()
|
public async Task Save()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var serializer = new JavaScriptSerializer();
|
using var cacheStream = File.Open(CacheFilePath, FileMode.Create, FileAccess.Write);
|
||||||
File.WriteAllText(CacheFilePath, serializer.Serialize(_storage));
|
await SerializeAsync(cacheStream, _storage).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
179
ChineseEncoding.cs
Normal file
179
ChineseEncoding.cs
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Ra3.BattleNet.Database.Utils;
|
||||||
|
|
||||||
|
public static class ChineseEncoding
|
||||||
|
{
|
||||||
|
private const int EncodedChineseTotalLength = 120;
|
||||||
|
private const int EncodedChineseChecksumLength = 3;
|
||||||
|
private const int EncodedChineseContentLength = EncodedChineseTotalLength - EncodedChineseChecksumLength;
|
||||||
|
private const int ChineseCodeSize = 13;
|
||||||
|
private const int ChineseCodeMask = 0b1111111111111;
|
||||||
|
private const int AsciiSectionSize = 0x80;
|
||||||
|
private const int Gb2312EucOffset = 0xA0;
|
||||||
|
private const int Gb2312RowWidth = 94;
|
||||||
|
private const int Gb2312LastRow = 87;
|
||||||
|
private const int Gb2312FirstUnassignedSectionBegin = 10;
|
||||||
|
private const int Gb2312FirstUnassignedSectionSize = 6;
|
||||||
|
private const int Gb2312SecondUnassignedSectionBegin = 88;
|
||||||
|
private const int Base64CodeSize = 6;
|
||||||
|
private const int Base64CodeMask = 0b111111;
|
||||||
|
private const string Base64Table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
||||||
|
|
||||||
|
public static string DecodeChineseFromBase64(string original)
|
||||||
|
{
|
||||||
|
BigInteger bits = 0;
|
||||||
|
int index = 0;
|
||||||
|
foreach (var c in original)
|
||||||
|
{
|
||||||
|
BigInteger value = Base64Table.IndexOf(c);
|
||||||
|
if (value == -1)
|
||||||
|
{
|
||||||
|
throw new Exception("Invalid base64 string");
|
||||||
|
}
|
||||||
|
bits = bits | (value << index);
|
||||||
|
index += Base64CodeSize;
|
||||||
|
}
|
||||||
|
BigInteger checksum = 0;
|
||||||
|
var result = new List<byte>();
|
||||||
|
for (var bitIndex = 0; bitIndex < EncodedChineseContentLength; bitIndex += ChineseCodeSize)
|
||||||
|
{
|
||||||
|
int value = (int)((bits >> bitIndex) & ChineseCodeMask);
|
||||||
|
checksum += value;
|
||||||
|
if (value == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (value < AsciiSectionSize)
|
||||||
|
{
|
||||||
|
if (value < 0x20 || value == 0x7F)
|
||||||
|
{
|
||||||
|
throw new Exception("Invalid ASCII");
|
||||||
|
}
|
||||||
|
result.Add((byte)value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
value -= AsciiSectionSize;
|
||||||
|
var index1 = value / Gb2312RowWidth + 1;
|
||||||
|
var index2 = value % Gb2312RowWidth + 1;
|
||||||
|
if (index1 >= Gb2312FirstUnassignedSectionBegin)
|
||||||
|
{
|
||||||
|
index1 += Gb2312FirstUnassignedSectionSize;
|
||||||
|
}
|
||||||
|
if (index1 >= Gb2312SecondUnassignedSectionBegin)
|
||||||
|
{
|
||||||
|
throw new Exception("Invalid first byte");
|
||||||
|
}
|
||||||
|
if (index2 > Gb2312RowWidth)
|
||||||
|
{
|
||||||
|
throw new Exception("Invalid second byte");
|
||||||
|
}
|
||||||
|
index1 += Gb2312EucOffset;
|
||||||
|
index2 += Gb2312EucOffset;
|
||||||
|
result.Add((byte)index1);
|
||||||
|
result.Add((byte)index2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((bits >> EncodedChineseContentLength) != checksum % 8)
|
||||||
|
{
|
||||||
|
throw new Exception("Invalid checksum");
|
||||||
|
}
|
||||||
|
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||||
|
return Encoding.GetEncoding(936).GetString(result.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string EncodeChineseToBase64(string original)
|
||||||
|
{
|
||||||
|
BigInteger bits = 0;
|
||||||
|
int index = 0;
|
||||||
|
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||||
|
var bytes = Encoding.GetEncoding(936).GetBytes(original);
|
||||||
|
BigInteger checksum = 0;
|
||||||
|
for (var i = 0; i < bytes.Length;)
|
||||||
|
{
|
||||||
|
BigInteger c = bytes[i];
|
||||||
|
if (c < AsciiSectionSize)
|
||||||
|
{
|
||||||
|
if (c < 0x20 || c == 0x7F)
|
||||||
|
{
|
||||||
|
throw new Exception("Invalid ASCII");
|
||||||
|
}
|
||||||
|
bits = bits | (c << index);
|
||||||
|
index += ChineseCodeSize;
|
||||||
|
checksum += c;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (c <= Gb2312EucOffset || c > (Gb2312EucOffset + Gb2312LastRow))
|
||||||
|
{
|
||||||
|
throw new Exception("Invalid first byte");
|
||||||
|
}
|
||||||
|
BigInteger c2 = bytes[i + 1];
|
||||||
|
if (c2 <= Gb2312EucOffset || c2 > (Gb2312EucOffset + Gb2312RowWidth))
|
||||||
|
{
|
||||||
|
throw new Exception("Invalid second byte");
|
||||||
|
}
|
||||||
|
var index1 = c - Gb2312EucOffset;
|
||||||
|
if (index1 >= Gb2312FirstUnassignedSectionBegin)
|
||||||
|
{
|
||||||
|
var offset = index1 - Gb2312FirstUnassignedSectionBegin;
|
||||||
|
if (offset < Gb2312FirstUnassignedSectionSize)
|
||||||
|
{
|
||||||
|
throw new Exception("AA-AF user defined zone not supported");
|
||||||
|
}
|
||||||
|
index1 -= Gb2312FirstUnassignedSectionSize;
|
||||||
|
}
|
||||||
|
var index2 = c2 - Gb2312EucOffset;
|
||||||
|
if (index2 > Gb2312RowWidth)
|
||||||
|
{
|
||||||
|
throw new Exception("Invalid second byte");
|
||||||
|
}
|
||||||
|
var value = (index1 - 1) * Gb2312RowWidth + (index2 - 1);
|
||||||
|
value = AsciiSectionSize + value;
|
||||||
|
bits = bits | (value << index);
|
||||||
|
index += ChineseCodeSize;
|
||||||
|
checksum += value;
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bits = bits | ((checksum % 8) << EncodedChineseContentLength);
|
||||||
|
var result = "";
|
||||||
|
var bitsIndex = 0;
|
||||||
|
while (bitsIndex < EncodedChineseTotalLength)
|
||||||
|
{
|
||||||
|
int v = (int)((bits >> bitsIndex) & Base64CodeMask);
|
||||||
|
result += Base64Table[v];
|
||||||
|
bitsIndex += Base64CodeSize;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetPrettyName(string name)
|
||||||
|
{
|
||||||
|
if (name.Length != 20)
|
||||||
|
{
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
foreach (var c in name)
|
||||||
|
{
|
||||||
|
if (!Base64Table.Contains(c))
|
||||||
|
{
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return DecodeChineseFromBase64(name);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
303
CommandChunk.cs
303
CommandChunk.cs
@ -1,303 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
|
||||||
{
|
|
||||||
internal static class RA3Commands
|
|
||||||
{
|
|
||||||
//public static IReadOnlyDictionary<byte, Func<BinaryReader, string>> CommandParser { get; private set; }
|
|
||||||
|
|
||||||
public static IReadOnlyDictionary<byte, Action<BinaryReader>> CommandParser => _commandParser;
|
|
||||||
public static IReadOnlyDictionary<byte, string> CommandNames => _commandNames;
|
|
||||||
|
|
||||||
private static Dictionary<byte, Action<BinaryReader>> _commandParser;
|
|
||||||
private static Dictionary<byte, string> _commandNames;
|
|
||||||
|
|
||||||
static RA3Commands()
|
|
||||||
{
|
|
||||||
Action<BinaryReader> fixedSizeParser(byte command, int size)
|
|
||||||
{
|
|
||||||
return (BinaryReader current) =>
|
|
||||||
{
|
|
||||||
var lastByte = current.ReadBytes(size - 2).Last();
|
|
||||||
if (lastByte != 0xFF)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException($"Failed to parse command {command:X}, last byte is {lastByte:X}");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Action<BinaryReader> variableSizeParser (byte command, int offset)
|
|
||||||
{
|
|
||||||
return (BinaryReader current) =>
|
|
||||||
{
|
|
||||||
var totalBytes = 2;
|
|
||||||
totalBytes += current.ReadBytes(offset - 2).Length;
|
|
||||||
for(var x = current.ReadByte(); x != 0xFF; x = current.ReadByte())
|
|
||||||
{
|
|
||||||
totalBytes += 1;
|
|
||||||
|
|
||||||
|
|
||||||
var size = ((x >> 4) + 1) * 4;
|
|
||||||
totalBytes += current.ReadBytes(size).Length;
|
|
||||||
}
|
|
||||||
totalBytes += 1;
|
|
||||||
var chk = totalBytes;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
var list = new List<(byte, Func<byte, int, Action<BinaryReader>>, int, string)>
|
|
||||||
{
|
|
||||||
(0x00, fixedSizeParser, 45, "展开建筑/建造碉堡(?)"),
|
|
||||||
(0x03, fixedSizeParser, 17, "开始升级"),
|
|
||||||
(0x04, fixedSizeParser, 17, "暂停/中止升级"),
|
|
||||||
(0x05, fixedSizeParser, 20, "开始生产单位或纳米核心"),
|
|
||||||
(0x06, fixedSizeParser, 20, "暂停/取消生产单位或纳米核心"),
|
|
||||||
(0x07, fixedSizeParser, 17, "开始建造建筑"),
|
|
||||||
(0x08, fixedSizeParser, 17, "暂停/取消建造建筑"),
|
|
||||||
(0x09, fixedSizeParser, 35, "摆放建筑"),
|
|
||||||
(0x0F, fixedSizeParser, 16, "(未知指令)"),
|
|
||||||
(0x14, fixedSizeParser, 16, "移动"),
|
|
||||||
(0x15, fixedSizeParser, 16, "移动攻击(A)"),
|
|
||||||
(0x16, fixedSizeParser, 16, "强制移动/碾压(G)"),
|
|
||||||
(0x21, fixedSizeParser, 20, "[游戏每3秒自动产生的指令]"),
|
|
||||||
(0x2C, fixedSizeParser, 29, "队形移动(左右键)"),
|
|
||||||
(0x32, fixedSizeParser, 53, "释放技能或协议(多个目标,如侦察扫描协议)"),
|
|
||||||
(0x34, fixedSizeParser, 45, "[游戏自动产生的UUID]"),
|
|
||||||
(0x35, fixedSizeParser, 1049, "[玩家信息(?)]"),
|
|
||||||
(0x36, fixedSizeParser, 16, "倒车移动(D)"),
|
|
||||||
(0x5F, fixedSizeParser, 11, "(未知指令)"),
|
|
||||||
|
|
||||||
(0x0A, variableSizeParser, 2, "出售建筑"),
|
|
||||||
(0x0D, variableSizeParser, 2, "右键攻击"),
|
|
||||||
(0x0E, variableSizeParser, 2, "强制攻击(Ctrl)"),
|
|
||||||
(0x12, variableSizeParser, 2, "(未知指令)"),
|
|
||||||
(0x1A, variableSizeParser, 2, "停止(S)"),
|
|
||||||
(0x1B, variableSizeParser, 2, "(未知指令)"),
|
|
||||||
(0x28, variableSizeParser, 2, "开始维修建筑"),
|
|
||||||
(0x29, variableSizeParser, 2, "停止维修建筑"),
|
|
||||||
(0x2A, variableSizeParser, 2, "选择所有单位(Q)"),
|
|
||||||
(0x2E, variableSizeParser, 2, "切换警戒/侵略/固守/停火模式"),
|
|
||||||
(0x2F, variableSizeParser, 2, "路径点模式(Alt)(?)"),
|
|
||||||
(0x37, variableSizeParser, 2, "[游戏不定期自动产生的指令]"),
|
|
||||||
(0x47, variableSizeParser, 2, "[游戏在第五帧自动产生的指令]"),
|
|
||||||
(0x48, variableSizeParser, 2, "(未知指令)"),
|
|
||||||
(0x4C, variableSizeParser, 2, "删除信标(或F9?)"),
|
|
||||||
(0x4E, variableSizeParser, 2, "选择协议"),
|
|
||||||
(0x52, variableSizeParser, 2, "(未知指令)"),
|
|
||||||
(0xF5, variableSizeParser, 5, "选择单位"),
|
|
||||||
(0xF6, variableSizeParser, 5, "[未知指令,貌似会在展开兵营核心时自动产生?]"),
|
|
||||||
(0xF8, variableSizeParser, 4, "鼠标左键单击/取消选择"),
|
|
||||||
(0xF9, variableSizeParser, 2, "[可能是步兵自行从进驻的建筑撤出]"),
|
|
||||||
(0xFA, variableSizeParser, 7, "创建编队"),
|
|
||||||
(0xFB, variableSizeParser, 2, "选择编队"),
|
|
||||||
(0xFC, variableSizeParser, 2, "(未知指令)"),
|
|
||||||
(0xFD, variableSizeParser, 7, "(未知指令)"),
|
|
||||||
(0xFE, variableSizeParser, 15, "释放技能或协议(无目标)"),
|
|
||||||
(0xFF, variableSizeParser, 34, "释放技能或协议(单个目标)"),
|
|
||||||
};
|
|
||||||
|
|
||||||
var specialList = new List<(byte, Action<BinaryReader>, string)>
|
|
||||||
{
|
|
||||||
(0x01, ParseSpecialChunk0x01, "[游戏自动生成的指令]"),
|
|
||||||
(0x02, ParseSetRallyPoint0x02, "设计集结点"),
|
|
||||||
(0x0C, ParseUngarrison0x0C, "从进驻的建筑撤出(?)"),
|
|
||||||
(0x10, ParseGarrison0x10, "进驻建筑"),
|
|
||||||
(0x33, ParseUUID0x33, "[游戏自动生成的UUID指令]"),
|
|
||||||
(0x4B, ParsePlaceBeacon0x4B, "信标")
|
|
||||||
};
|
|
||||||
|
|
||||||
_commandParser = new Dictionary<byte, Action<BinaryReader>>();
|
|
||||||
_commandNames = new Dictionary<byte, string>();
|
|
||||||
|
|
||||||
foreach (var (id, maker, size, description) in list)
|
|
||||||
{
|
|
||||||
_commandParser.Add(id, maker(id, size));
|
|
||||||
_commandNames.Add(id, description);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var (id, parser, description) in specialList)
|
|
||||||
{
|
|
||||||
_commandParser.Add(id, parser);
|
|
||||||
_commandNames.Add(id, description);
|
|
||||||
}
|
|
||||||
|
|
||||||
_commandNames.Add(0x4D, "在信标里输入文字");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void UnknownCommandParser(BinaryReader current, byte commandID)
|
|
||||||
{
|
|
||||||
while(true)
|
|
||||||
{
|
|
||||||
var value = current.ReadByte();
|
|
||||||
if (value == 0xFF)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//return $"(未知指令 0x{commandID:2X})";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string GetCommandName(byte commandID)
|
|
||||||
{
|
|
||||||
return CommandNames.TryGetValue(commandID, out var storedName) ? storedName : $"(未知指令 0x{commandID:2X})";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ParseSpecialChunk0x01(BinaryReader current)
|
|
||||||
{
|
|
||||||
var firstByte = current.ReadByte();
|
|
||||||
if (firstByte == 0xFF)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sixthByte = current.ReadBytes(5).Last();
|
|
||||||
if(sixthByte == 0xFF)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sixteenthByte = current.ReadBytes(10).Last();
|
|
||||||
var size = (int)(sixteenthByte + 1) * 4 + 14;
|
|
||||||
var lastByte = current.ReadBytes(size).Last();
|
|
||||||
if(lastByte != 0xFF)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException();
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ParseSetRallyPoint0x02(BinaryReader current)
|
|
||||||
{
|
|
||||||
var size = (current.ReadBytes(23).Last() + 1) * 2 + 1;
|
|
||||||
var lastByte = current.ReadBytes(size).Last();
|
|
||||||
if (lastByte != 0xFF)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ParseUngarrison0x0C(BinaryReader current)
|
|
||||||
{
|
|
||||||
current.ReadByte();
|
|
||||||
var size = (current.ReadByte() + 1) * 4 + 1;
|
|
||||||
var lastByte = current.ReadBytes(size).Last();
|
|
||||||
if (lastByte != 0xFF)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ParseGarrison0x10(BinaryReader current)
|
|
||||||
{
|
|
||||||
var type = current.ReadByte();
|
|
||||||
var size = -1;
|
|
||||||
if(type == 0x14)
|
|
||||||
{
|
|
||||||
size = 9;
|
|
||||||
}
|
|
||||||
else if(type == 0x04)
|
|
||||||
{
|
|
||||||
size = 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastByte = current.ReadBytes(size).Last();
|
|
||||||
if(lastByte != 0xFF)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ParseUUID0x33(BinaryReader current)
|
|
||||||
{
|
|
||||||
current.ReadByte();
|
|
||||||
var firstStringLength = (int)current.ReadByte();
|
|
||||||
current.ReadBytes(firstStringLength + 1);
|
|
||||||
var secondStringLength = current.ReadByte() * 2;
|
|
||||||
var lastByte = current.ReadBytes(secondStringLength + 6).Last();
|
|
||||||
if (lastByte != 0xFF)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ParsePlaceBeacon0x4B(BinaryReader current)
|
|
||||||
{
|
|
||||||
var type = current.ReadByte();
|
|
||||||
var size = -1;
|
|
||||||
if(type == 0x04)
|
|
||||||
{
|
|
||||||
size = 5;
|
|
||||||
}
|
|
||||||
else if(type == 0x07)
|
|
||||||
{
|
|
||||||
size = 13;
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastByte = current.ReadBytes(size).Last();
|
|
||||||
if (lastByte != 0xFF)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class CommandChunk
|
|
||||||
{
|
|
||||||
public byte CommandID { get; private set; }
|
|
||||||
public int PlayerIndex { get; private set; }
|
|
||||||
|
|
||||||
public static List<CommandChunk> Parse(in ReplayChunk chunk)
|
|
||||||
{
|
|
||||||
if (chunk.Type != 1)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
int ManglePlayerID(byte ID)
|
|
||||||
{
|
|
||||||
return ID / 8 - 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
using (var stream = new MemoryStream(chunk.Data))
|
|
||||||
using (var reader = new BinaryReader(stream))
|
|
||||||
{
|
|
||||||
if(reader.ReadByte() != 1)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException("Payload first byte not 1");
|
|
||||||
}
|
|
||||||
|
|
||||||
var list = new List<CommandChunk>();
|
|
||||||
var numberOfCommands = reader.ReadInt32();
|
|
||||||
for(var i = 0; i < numberOfCommands; ++i)
|
|
||||||
{
|
|
||||||
var commandID = reader.ReadByte();
|
|
||||||
var playerID = reader.ReadByte();
|
|
||||||
if (RA3Commands.CommandParser.TryGetValue(commandID, out var parser))
|
|
||||||
{
|
|
||||||
RA3Commands.CommandParser[commandID](reader);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
RA3Commands.UnknownCommandParser(reader, commandID);
|
|
||||||
}
|
|
||||||
|
|
||||||
list.Add(new CommandChunk { CommandID = commandID, PlayerIndex = ManglePlayerID(playerID) });
|
|
||||||
}
|
|
||||||
|
|
||||||
if(reader.BaseStream.Position != reader.BaseStream.Length)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException("Payload not fully parsed");
|
|
||||||
}
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
20
Debug.xaml
20
Debug.xaml
@ -6,8 +6,20 @@
|
|||||||
xmlns:local="clr-namespace:AnotherReplayReader"
|
xmlns:local="clr-namespace:AnotherReplayReader"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="Debug" Height="450" Width="800">
|
Title="Debug" Height="450" Width="800">
|
||||||
<Grid>
|
<DockPanel Margin="10,10,10,10">
|
||||||
<TextBox x:Name="_textBox" Margin="10,34,10,10" TextWrapping="Wrap" Text="{Binding Path=DebugMessage, Mode=TwoWay}" ScrollViewer.VerticalScrollBarVisibility="Auto"/>
|
<StackPanel DockPanel.Dock="Top"
|
||||||
<Button x:Name="_export" Content="导出日志" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="75" Click="OnExport_Click"/>
|
Orientation="Horizontal"
|
||||||
</Grid>
|
Margin="0,0,0,10">
|
||||||
|
<Button Padding="8,2"
|
||||||
|
Margin="0,0,10,0"
|
||||||
|
Content="导出日志"
|
||||||
|
Click="OnExportButtonClick" />
|
||||||
|
<Button Padding="8,2"
|
||||||
|
Content="清空日志"
|
||||||
|
Click="OnClearButtonClick" />
|
||||||
|
</StackPanel>
|
||||||
|
<TextBox x:Name="_textBox"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
ScrollViewer.VerticalScrollBarVisibility="Auto" />
|
||||||
|
</DockPanel>
|
||||||
</Window>
|
</Window>
|
||||||
|
127
Debug.xaml.cs
127
Debug.xaml.cs
@ -1,68 +1,119 @@
|
|||||||
using System;
|
using AnotherReplayReader.Utils;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
|
||||||
using System.Windows.Data;
|
|
||||||
using System.Windows.Documents;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using System.Windows.Media;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using System.Windows.Shapes;
|
|
||||||
using Microsoft.Win32;
|
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
namespace AnotherReplayReader
|
||||||
{
|
{
|
||||||
public sealed class DebugMessageWrapper : INotifyPropertyChanged
|
public sealed class DebugMessageWrapper
|
||||||
{
|
{
|
||||||
public event PropertyChangedEventHandler PropertyChanged;
|
public readonly struct Proxy
|
||||||
public string DebugMessage
|
|
||||||
{
|
{
|
||||||
get => _debugMessage;
|
public readonly string Payload;
|
||||||
set
|
public Proxy(string text)
|
||||||
{
|
{
|
||||||
_debugMessage = value;
|
Payload = text;
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("DebugMessage"));
|
}
|
||||||
|
public static Proxy operator +(Proxy p, string text)
|
||||||
|
{
|
||||||
|
return string.IsNullOrEmpty(p.Payload) ? new Proxy(text) : new Proxy(p.Payload + text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _debugMessage;
|
private readonly object _lock = new();
|
||||||
|
private readonly List<string> _list = new();
|
||||||
|
|
||||||
|
public event Action<string>? NewText;
|
||||||
|
public Proxy DebugMessage
|
||||||
|
{
|
||||||
|
get => new();
|
||||||
|
set
|
||||||
|
{
|
||||||
|
var text = value.Payload;
|
||||||
|
using var locker = new Lock(_lock);
|
||||||
|
if (NewText is null || _list.Count > 0)
|
||||||
|
{
|
||||||
|
_list.Add(text);
|
||||||
|
if (NewText is not null)
|
||||||
|
{
|
||||||
|
text = string.Join(string.Empty, _list);
|
||||||
|
_list.Clear();
|
||||||
|
_list.Capacity = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NewText(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event Action? RequestedSave;
|
||||||
|
public void RequestSave() => RequestedSave?.Invoke();
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Debug.xaml 的交互逻辑
|
/// Debug.xaml 的交互逻辑
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class Debug : Window
|
public sealed partial class Debug : Window
|
||||||
{
|
{
|
||||||
public static readonly DebugMessageWrapper Instance = new DebugMessageWrapper();
|
public static readonly DebugMessageWrapper Instance = new();
|
||||||
|
private static readonly object _lock = new();
|
||||||
|
private static Debug? _window = null;
|
||||||
|
|
||||||
public Debug()
|
public static void Initialize()
|
||||||
{
|
{
|
||||||
DataContext = Instance;
|
using var locker = new Lock(_lock);
|
||||||
InitializeComponent();
|
if (_window is not null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var window = new Debug();
|
||||||
|
window.InitializeComponent();
|
||||||
|
_window = window;
|
||||||
|
Instance.NewText += _window.AppendText;
|
||||||
|
Instance.RequestedSave += _window.ExportInMainWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnExport_Click(object sender, RoutedEventArgs e)
|
public static new void ShowDialog() => Lock.Run(_lock, () => (_window as Window)?.ShowDialog());
|
||||||
{
|
|
||||||
var saveFileDialog = new SaveFileDialog
|
|
||||||
{
|
|
||||||
Filter = "文本文档 (*.txt)|*.txt|所有文件 (*.*)|*.*",
|
|
||||||
OverwritePrompt = true,
|
|
||||||
};
|
|
||||||
|
|
||||||
var result = saveFileDialog.ShowDialog(this);
|
protected override void OnClosing(CancelEventArgs e)
|
||||||
if (result == true)
|
{
|
||||||
|
e.Cancel = true;
|
||||||
|
Hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Debug() { }
|
||||||
|
|
||||||
|
private void OnExportButtonClick(object sender, RoutedEventArgs e) => Export(this);
|
||||||
|
|
||||||
|
private void OnClearButtonClick(object sender, RoutedEventArgs e) => _textBox.Clear();
|
||||||
|
|
||||||
|
private void AppendText(string s) => Dispatcher.InvokeAsync(() => _textBox.AppendText(s));
|
||||||
|
|
||||||
|
private void ExportInMainWindow() => Export(Application.Current.MainWindow);
|
||||||
|
|
||||||
|
private void Export(Window owner)
|
||||||
|
{
|
||||||
|
Dispatcher.Invoke(() =>
|
||||||
{
|
{
|
||||||
using (var file = saveFileDialog.OpenFile())
|
var saveFileDialog = new SaveFileDialog
|
||||||
using (var writer = new StreamWriter(file))
|
|
||||||
{
|
{
|
||||||
writer.Write(Instance.DebugMessage);
|
Filter = "文本文档 (*.txt)|*.txt|所有文件 (*.*)|*.*",
|
||||||
|
OverwritePrompt = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = saveFileDialog.ShowDialog(owner);
|
||||||
|
if (result == true)
|
||||||
|
{
|
||||||
|
using var file = saveFileDialog.OpenFile();
|
||||||
|
using var writer = new StreamWriter(file);
|
||||||
|
writer.Write(_textBox.Text);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 +0,0 @@
|
|||||||
{
|
|
||||||
"General": {
|
|
||||||
"InputAssemblies": [
|
|
||||||
"$(TargetDir)Microsoft.DotNet.PlatformAbstractions.dll",
|
|
||||||
"$(TargetDir)Microsoft.Extensions.DependencyModel.dll",
|
|
||||||
"$(TargetDir)Microsoft.Win32.Primitives.dll",
|
|
||||||
"$(TargetDir)NativeLibraryLoader.dll",
|
|
||||||
"$(TargetDir)netstandard.dll",
|
|
||||||
"$(TargetDir)Newtonsoft.Json.dll",
|
|
||||||
"$(TargetDir)NLog.dll",
|
|
||||||
"$(TargetDir)OpenSage.Core.dll",
|
|
||||||
"$(TargetDir)OpenSage.FileFormats.Big.dll",
|
|
||||||
"$(TargetDir)OpenSage.FileFormats.dll",
|
|
||||||
"$(TargetDir)OpenSage.FileFormats.RefPack.dll",
|
|
||||||
"$(TargetDir)OpenSage.Mathematics.dll",
|
|
||||||
"$(TargetDir)Pfim.dll",
|
|
||||||
"$(TargetDir)System.AppContext.dll",
|
|
||||||
"$(TargetDir)System.Collections.Concurrent.dll",
|
|
||||||
"$(TargetDir)System.Collections.dll",
|
|
||||||
"$(TargetDir)System.Collections.NonGeneric.dll",
|
|
||||||
"$(TargetDir)System.Collections.Specialized.dll",
|
|
||||||
"$(TargetDir)System.ComponentModel.dll",
|
|
||||||
"$(TargetDir)System.ComponentModel.EventBasedAsync.dll",
|
|
||||||
"$(TargetDir)System.ComponentModel.Primitives.dll",
|
|
||||||
"$(TargetDir)System.ComponentModel.TypeConverter.dll",
|
|
||||||
"$(TargetDir)System.Console.dll",
|
|
||||||
"$(TargetDir)System.Data.Common.dll",
|
|
||||||
"$(TargetDir)System.Diagnostics.Contracts.dll",
|
|
||||||
"$(TargetDir)System.Diagnostics.Debug.dll",
|
|
||||||
"$(TargetDir)System.Diagnostics.DiagnosticSource.dll",
|
|
||||||
"$(TargetDir)System.Diagnostics.FileVersionInfo.dll",
|
|
||||||
"$(TargetDir)System.Diagnostics.Process.dll",
|
|
||||||
"$(TargetDir)System.Diagnostics.StackTrace.dll",
|
|
||||||
"$(TargetDir)System.Diagnostics.TextWriterTraceListener.dll",
|
|
||||||
"$(TargetDir)System.Diagnostics.Tools.dll",
|
|
||||||
"$(TargetDir)System.Diagnostics.TraceSource.dll",
|
|
||||||
"$(TargetDir)System.Diagnostics.Tracing.dll",
|
|
||||||
"$(TargetDir)System.Drawing.Primitives.dll",
|
|
||||||
"$(TargetDir)System.Dynamic.Runtime.dll",
|
|
||||||
"$(TargetDir)System.Globalization.Calendars.dll",
|
|
||||||
"$(TargetDir)System.Globalization.dll",
|
|
||||||
"$(TargetDir)System.Globalization.Extensions.dll",
|
|
||||||
"$(TargetDir)System.IO.Compression.dll",
|
|
||||||
"$(TargetDir)System.IO.Compression.ZipFile.dll",
|
|
||||||
"$(TargetDir)System.IO.dll",
|
|
||||||
"$(TargetDir)System.IO.FileSystem.dll",
|
|
||||||
"$(TargetDir)System.IO.FileSystem.DriveInfo.dll",
|
|
||||||
"$(TargetDir)System.IO.FileSystem.Primitives.dll",
|
|
||||||
"$(TargetDir)System.IO.FileSystem.Watcher.dll",
|
|
||||||
"$(TargetDir)System.IO.IsolatedStorage.dll",
|
|
||||||
"$(TargetDir)System.IO.MemoryMappedFiles.dll",
|
|
||||||
"$(TargetDir)System.IO.Pipes.dll",
|
|
||||||
"$(TargetDir)System.IO.UnmanagedMemoryStream.dll",
|
|
||||||
"$(TargetDir)System.Linq.dll",
|
|
||||||
"$(TargetDir)System.Linq.Expressions.dll",
|
|
||||||
"$(TargetDir)System.Linq.Parallel.dll",
|
|
||||||
"$(TargetDir)System.Linq.Queryable.dll",
|
|
||||||
"$(TargetDir)System.Net.Http.dll",
|
|
||||||
"$(TargetDir)System.Net.NameResolution.dll",
|
|
||||||
"$(TargetDir)System.Net.NetworkInformation.dll",
|
|
||||||
"$(TargetDir)System.Net.Ping.dll",
|
|
||||||
"$(TargetDir)System.Net.Primitives.dll",
|
|
||||||
"$(TargetDir)System.Net.Requests.dll",
|
|
||||||
"$(TargetDir)System.Net.Security.dll",
|
|
||||||
"$(TargetDir)System.Net.Sockets.dll",
|
|
||||||
"$(TargetDir)System.Net.WebHeaderCollection.dll",
|
|
||||||
"$(TargetDir)System.Net.WebSockets.Client.dll",
|
|
||||||
"$(TargetDir)System.Net.WebSockets.dll",
|
|
||||||
"$(TargetDir)System.Numerics.Vectors.dll",
|
|
||||||
"$(TargetDir)System.ObjectModel.dll",
|
|
||||||
"$(TargetDir)System.Reflection.dll",
|
|
||||||
"$(TargetDir)System.Reflection.Extensions.dll",
|
|
||||||
"$(TargetDir)System.Reflection.Primitives.dll",
|
|
||||||
"$(TargetDir)System.Resources.Reader.dll",
|
|
||||||
"$(TargetDir)System.Resources.ResourceManager.dll",
|
|
||||||
"$(TargetDir)System.Resources.Writer.dll",
|
|
||||||
"$(TargetDir)System.Runtime.CompilerServices.Unsafe.dll",
|
|
||||||
"$(TargetDir)System.Runtime.CompilerServices.VisualC.dll",
|
|
||||||
"$(TargetDir)System.Runtime.dll",
|
|
||||||
"$(TargetDir)System.Runtime.Extensions.dll",
|
|
||||||
"$(TargetDir)System.Runtime.Handles.dll",
|
|
||||||
"$(TargetDir)System.Runtime.InteropServices.dll",
|
|
||||||
"$(TargetDir)System.Runtime.InteropServices.RuntimeInformation.dll",
|
|
||||||
"$(TargetDir)System.Runtime.Numerics.dll",
|
|
||||||
"$(TargetDir)System.Runtime.Serialization.Formatters.dll",
|
|
||||||
"$(TargetDir)System.Runtime.Serialization.Json.dll",
|
|
||||||
"$(TargetDir)System.Runtime.Serialization.Primitives.dll",
|
|
||||||
"$(TargetDir)System.Runtime.Serialization.Xml.dll",
|
|
||||||
"$(TargetDir)System.Security.Claims.dll",
|
|
||||||
"$(TargetDir)System.Security.Cryptography.Algorithms.dll",
|
|
||||||
"$(TargetDir)System.Security.Cryptography.Csp.dll",
|
|
||||||
"$(TargetDir)System.Security.Cryptography.Encoding.dll",
|
|
||||||
"$(TargetDir)System.Security.Cryptography.Primitives.dll",
|
|
||||||
"$(TargetDir)System.Security.Cryptography.X509Certificates.dll",
|
|
||||||
"$(TargetDir)System.Security.Principal.dll",
|
|
||||||
"$(TargetDir)System.Security.SecureString.dll",
|
|
||||||
"$(TargetDir)System.Text.Encoding.CodePages.dll",
|
|
||||||
"$(TargetDir)System.Text.Encoding.dll",
|
|
||||||
"$(TargetDir)System.Text.Encoding.Extensions.dll",
|
|
||||||
"$(TargetDir)System.Text.RegularExpressions.dll",
|
|
||||||
"$(TargetDir)System.Threading.dll",
|
|
||||||
"$(TargetDir)System.Threading.Overlapped.dll",
|
|
||||||
"$(TargetDir)System.Threading.Tasks.dll",
|
|
||||||
"$(TargetDir)System.Threading.Tasks.Parallel.dll",
|
|
||||||
"$(TargetDir)System.Threading.Thread.dll",
|
|
||||||
"$(TargetDir)System.Threading.ThreadPool.dll",
|
|
||||||
"$(TargetDir)System.Threading.Timer.dll",
|
|
||||||
"$(TargetDir)System.ValueTuple.dll",
|
|
||||||
"$(TargetDir)System.Xml.ReaderWriter.dll",
|
|
||||||
"$(TargetDir)System.Xml.XDocument.dll",
|
|
||||||
"$(TargetDir)System.Xml.XmlDocument.dll",
|
|
||||||
"$(TargetDir)System.Xml.XmlSerializer.dll",
|
|
||||||
"$(TargetDir)System.Xml.XPath.dll",
|
|
||||||
"$(TargetDir)System.Xml.XPath.XDocument.dll"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Advanced": {
|
|
||||||
"AllowWildCards": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
44
IpAndPlayer.cs
Normal file
44
IpAndPlayer.cs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
using AnotherReplayReader.Utils;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader
|
||||||
|
{
|
||||||
|
public sealed class IpAndPlayer
|
||||||
|
{
|
||||||
|
public static string SimpleIPToString(uint ip)
|
||||||
|
{
|
||||||
|
return $"{ip / 256 / 256 / 256}.{ip / 256 / 256 % 256}.{ip / 256 % 256}.{ip % 256}";
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("IP")]
|
||||||
|
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
|
||||||
|
public uint Ip
|
||||||
|
{
|
||||||
|
get => _ip;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_ip = value;
|
||||||
|
IpString = SimpleIPToString(_ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[JsonIgnore]
|
||||||
|
public string IpString { get; private set; } = "0.0.0.0";
|
||||||
|
|
||||||
|
[JsonPropertyName("ID")]
|
||||||
|
public string Id
|
||||||
|
{
|
||||||
|
get => _id;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_id = value;
|
||||||
|
_pinyin = _id.ToPinyin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[JsonIgnore]
|
||||||
|
public string? PinyinId => _pinyin;
|
||||||
|
|
||||||
|
private uint _ip;
|
||||||
|
private string _id = string.Empty;
|
||||||
|
private string? _pinyin;
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,9 @@
|
|||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:local="clr-namespace:AnotherReplayReader"
|
xmlns:local="clr-namespace:AnotherReplayReader"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="MainWindow" Width="800" Height="600">
|
Title="MainWindow" Width="800" Height="600"
|
||||||
|
WindowStartupLocation="CenterScreen"
|
||||||
|
Loaded="OnMainWindowLoaded">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="20"/>
|
<RowDefinition Height="20"/>
|
||||||
@ -22,8 +24,8 @@
|
|||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Label x:Name="label" Content="录像文件夹" Grid.Column="0" Margin="0,-3,5,8" HorizontalContentAlignment="Right" HorizontalAlignment="Right" Width="75"/>
|
<Label x:Name="label" Content="录像文件夹" Grid.Column="0" Margin="0,-3,5,8" HorizontalContentAlignment="Right" HorizontalAlignment="Right" Width="75"/>
|
||||||
<TextBox x:Name="_replayFolderPathBox" Grid.Column="1" TextWrapping="Wrap" Text="{Binding Path=ReplayFolderPath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextChanged="OnReplayFolderPathBoxTextChanged" Margin="0,0,0,10" Grid.ColumnSpan="2" />
|
<TextBox x:Name="_replayFolderPathBox" Grid.Column="1" TextWrapping="Wrap" Text="{Binding Path=ReplayFolderPath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextChanged="OnReplayFolderPathBoxTextChanged" Margin="0,0,0,10" Grid.ColumnSpan="2" />
|
||||||
<Button x:Name="_browseButton" Content="浏览..." Grid.Column="3" Margin="10,0,5,11" Click="OnBrowseButton_Click"/>
|
<Button x:Name="_browseButton" Content="浏览..." Grid.Column="3" Margin="10,0,5,11" Click="OnBrowseButtonClick"/>
|
||||||
<Button x:Name="_aboutButton" Content="关于..." Click="OnAboutButton_Click" Grid.Column="4" Margin="5,0,12,11"/>
|
<Button x:Name="_aboutButton" Content="关于..." Click="OnAboutButtonClick" Grid.Column="4" Margin="5,0,12,11"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid Grid.Row="2">
|
<Grid Grid.Row="2">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
@ -35,26 +37,57 @@
|
|||||||
<ColumnDefinition Width="145"/>
|
<ColumnDefinition Width="145"/>
|
||||||
<ColumnDefinition Width="472*"/>
|
<ColumnDefinition Width="472*"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<TextBox x:Name="_replayFilterBox" Grid.Row="0" Grid.Column="2" Margin="120,10,10,0" TextWrapping="Wrap" Height="20" VerticalAlignment="Top" />
|
<Grid Grid.Row="0"
|
||||||
|
Grid.Column="2"
|
||||||
|
Margin="120,10,10,0"
|
||||||
|
Height="20"
|
||||||
|
VerticalAlignment="Top">
|
||||||
|
<TextBox x:Name="_replayFilterBox"
|
||||||
|
TextChanged="OnReplayFilterBoxTextChanged" />
|
||||||
|
<TextBlock IsHitTestVisible="False"
|
||||||
|
Text="输入录像名称、玩家名称或地图名称等 可以筛选录像"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
Margin="10,0,0,0"
|
||||||
|
Foreground="DarkGray">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="{x:Type TextBlock}">
|
||||||
|
<Setter Property="Visibility"
|
||||||
|
Value="Collapsed" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding Text, ElementName=_replayFilterBox}"
|
||||||
|
Value="">
|
||||||
|
<Setter Property="Visibility"
|
||||||
|
Value="Visible" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<Button x:Name="_refreshButton" Content="刷新" Grid.Column="2" HorizontalAlignment="Left" Margin="0,11,0,0" VerticalAlignment="Top" Width="80" Click="OnReplayFolderPathBoxTextChanged"/>
|
<Button x:Name="_refreshButton" Content="刷新" Grid.Column="2" HorizontalAlignment="Left" Margin="0,11,0,0" VerticalAlignment="Top" Width="80" Click="OnReplayFolderPathBoxTextChanged"/>
|
||||||
<DataGrid x:Name="_dataGrid" Grid.Row="0" Grid.Column="2" Grid.RowSpan="2" Margin="0,30,10,16" SelectionMode="Single" SelectionChanged="OnReplaySelectionChanged">
|
<DataGrid x:Name="_dataGrid" Grid.Row="0" Grid.Column="2" Grid.RowSpan="2" Margin="0,30,10,16"
|
||||||
|
SelectionMode="Single" SelectionChanged="OnReplaySelectionChanged"
|
||||||
|
AutoGenerateColumns="False">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="文件名" Binding="{Binding Path=FileName}" Width="4*" IsReadOnly="True"/>
|
<DataGridTextColumn Header="文件名" Binding="{Binding Path=FileName}" Width="4*" IsReadOnly="True"/>
|
||||||
<DataGridTextColumn Header="玩家人数" Binding="{Binding Path=NumberOfPlayingPlayers}" Width="1.5*" IsReadOnly="True"/>
|
<DataGridTextColumn Header="玩家人数" Binding="{Binding Path=NumberOfPlayingPlayers}" Width="1.5*" IsReadOnly="True"/>
|
||||||
<DataGridTextColumn Header="录像时长" Binding="{Binding Path=Length, TargetNullValue='?'}" Width="1.65*" IsReadOnly="True"/>
|
<DataGridTextColumn Header="录像时长"
|
||||||
|
Binding="{Binding Path=Length, TargetNullValue='?'}" Width="1.65*" IsReadOnly="True"/>
|
||||||
<DataGridTextColumn Header="Mod" Binding="{Binding Path=Mod}" Width="1.5*" IsReadOnly="True"/>
|
<DataGridTextColumn Header="Mod" Binding="{Binding Path=Mod}" Width="1.5*" IsReadOnly="True"/>
|
||||||
<DataGridTextColumn Header="录像日期" Binding="{Binding Path=Date, StringFormat='{}{0:yyyy/MM/dd HH:mm:SS}'}" Width="3*" IsReadOnly="True" />
|
<DataGridTextColumn Header="录像日期" Binding="{Binding Path=Date, StringFormat='{}{0:yyyy/MM/dd HH:mm:SS}'}" Width="3*" IsReadOnly="True" />
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
<Button x:Name="_debugButton" Content="调试信息" Grid.Row="0" Grid.Column="0" VerticalAlignment="Top" Click="OnDebugButton_Click" Margin="20,6,0,0" HorizontalAlignment="Left" Width="80"/>
|
<Button x:Name="_debugButton" Content="调试信息" Grid.Row="0" Grid.Column="0" VerticalAlignment="Top" Click="OnDebugButtonClick" Margin="20,6,0,0" HorizontalAlignment="Left" Width="80"/>
|
||||||
<TextBox x:Name="_replayDetailsBox" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Margin="20,30,20,0" Text="{Binding Path=ReplayDetails}" TextWrapping="Wrap"/>
|
<TextBox x:Name="_replayDetailsBox" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Margin="20,30,20,0" Text="{Binding Path=ReplayDetails}" TextWrapping="Wrap"/>
|
||||||
<Image x:Name="_image" Grid.Row="1" Grid.Column="0" Margin="18,20,0,0" Stretch="Uniform" HorizontalAlignment="Left" Width="120" Height="120" VerticalAlignment="Top" />
|
<Image x:Name="_image" Grid.Row="1" Grid.Column="0" Margin="18,20,0,0" Stretch="Uniform" HorizontalAlignment="Left" Width="120" Height="120" VerticalAlignment="Top" />
|
||||||
<Button x:Name="_playButton" Grid.Row="1" Grid.Column="1" Content="播放录像"
|
<Button x:Name="_playButton" Grid.Row="1" Grid.Column="1" Content="播放录像"
|
||||||
Margin="10,0,20,96"
|
Margin="10,0,20,96"
|
||||||
Height="35" VerticalAlignment="Bottom" IsEnabled="{Binding Path=ReplayPlayable}" Click="OnPlayReplayButton_Click"/>
|
Height="35" VerticalAlignment="Bottom" IsEnabled="{Binding Path=ReplayPlayable}" Click="OnPlayReplayButtonClick"/>
|
||||||
<Button x:Name="_saveAsButton" Grid.Row="1" Grid.Column="1" Content="修复录像"
|
<Button x:Name="_saveAsButton" Grid.Row="1" Grid.Column="1" Content="修复录像"
|
||||||
Margin="10,0,20,56"
|
Margin="10,0,20,56"
|
||||||
Height="35" VerticalAlignment="Bottom" IsEnabled="{Binding Path=ReplayDamaged}" Click="OnFixReplayButton_Click"/>
|
Height="35" VerticalAlignment="Bottom" IsEnabled="{Binding Path=ReplayDamaged}" Click="OnFixReplayButtonClick"/>
|
||||||
<Button x:Name="_detailsButton"
|
<Button x:Name="_detailsButton"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
@ -63,7 +96,7 @@
|
|||||||
Height="35"
|
Height="35"
|
||||||
VerticalAlignment="Bottom"
|
VerticalAlignment="Bottom"
|
||||||
IsEnabled="{Binding Path=ReplaySelected}"
|
IsEnabled="{Binding Path=ReplaySelected}"
|
||||||
Click="OnDetailsButton_Click" />
|
Click="OnDetailsButtonClick" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
using Microsoft.Win32;
|
using AnotherReplayReader.ReplayFile;
|
||||||
|
using AnotherReplayReader.Utils;
|
||||||
|
using Microsoft.Win32;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@ -9,6 +12,7 @@ using System.Runtime.CompilerServices;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
namespace AnotherReplayReader
|
||||||
{
|
{
|
||||||
@ -16,21 +20,14 @@ namespace AnotherReplayReader
|
|||||||
{
|
{
|
||||||
public MainWindowProperties()
|
public MainWindowProperties()
|
||||||
{
|
{
|
||||||
string userDataLeafName = null;
|
const RegistryHive hklm = RegistryHive.LocalMachine;
|
||||||
string replayFolderName = null;
|
RA3Directory = RegistryUtils.RetrieveInRa3(hklm, "Install Dir");
|
||||||
using (var view32 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32))
|
string? userDataLeafName = RegistryUtils.RetrieveInRa3(hklm, "UserDataLeafName");
|
||||||
using (var ra3Key = view32.OpenSubKey(@"Software\Electronic Arts\Electronic Arts\Red Alert 3", false))
|
string? replayFolderName = RegistryUtils.RetrieveInRa3(hklm, "ReplayFolderName");
|
||||||
{
|
|
||||||
RA3Directory = ra3Key?.GetValue("Install Dir") as string;
|
|
||||||
userDataLeafName = ra3Key?.GetValue("UserDataLeafName") as string;
|
|
||||||
replayFolderName = ra3Key?.GetValue("ReplayFolderName") as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(userDataLeafName))
|
if (string.IsNullOrWhiteSpace(userDataLeafName))
|
||||||
{
|
{
|
||||||
userDataLeafName = "Red Alert 3";
|
userDataLeafName = "Red Alert 3";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(replayFolderName))
|
if (string.IsNullOrWhiteSpace(replayFolderName))
|
||||||
{
|
{
|
||||||
replayFolderName = "Replays";
|
replayFolderName = "Replays";
|
||||||
@ -42,17 +39,21 @@ namespace AnotherReplayReader
|
|||||||
ModsDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), userDataLeafName, "Mods");
|
ModsDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), userDataLeafName, "Mods");
|
||||||
}
|
}
|
||||||
|
|
||||||
public event PropertyChangedEventHandler PropertyChanged;
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
// This method is called by the Set accessor of each property.
|
// This method is called by the Set accessor of each property.
|
||||||
// The CallerMemberName attribute that is applied to the optional propertyName
|
// The CallerMemberName attribute that is applied to the optional propertyName
|
||||||
// parameter causes the property name of the caller to be substituted as an argument.
|
// parameter causes the property name of the caller to be substituted as an argument.
|
||||||
private void NotifyPropertyChanged<T>(T value, [CallerMemberName] string propertyName = "")
|
private void SetAndNotifyPropertyChanged<T>(ref T target, T newValue, [CallerMemberName] string propertyName = "")
|
||||||
{
|
{
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
if (!Equals(target, newValue))
|
||||||
|
{
|
||||||
|
target = newValue;
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string RA3Directory { get; }
|
public string? RA3Directory { get; }
|
||||||
public string RA3ReplayFolderPath { get; }
|
public string RA3ReplayFolderPath { get; }
|
||||||
public string RA3Exe => Path.Combine(RA3Directory, "RA3.exe");
|
public string RA3Exe => Path.Combine(RA3Directory, "RA3.exe");
|
||||||
public string CustomMapsDirectory { get; }
|
public string CustomMapsDirectory { get; }
|
||||||
@ -60,46 +61,38 @@ namespace AnotherReplayReader
|
|||||||
|
|
||||||
public string ReplayFolderPath
|
public string ReplayFolderPath
|
||||||
{
|
{
|
||||||
get { return _replayFolderPath; }
|
get => _replayFolderPath;
|
||||||
set { _replayFolderPath = value; NotifyPropertyChanged(_replayFolderPath); }
|
set => SetAndNotifyPropertyChanged(ref _replayFolderPath, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ReplayFilterString
|
public string? ReplayDetails
|
||||||
{
|
{
|
||||||
get { return _replayFilterString; }
|
get => _replayDetails;
|
||||||
set { _replayFilterString = value; NotifyPropertyChanged(_replayFilterString); }
|
set => SetAndNotifyPropertyChanged(ref _replayDetails, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ReplayDetails
|
public bool ReplaySelected => CurrentReplay != null;
|
||||||
|
|
||||||
|
public bool ReplayPlayable => CurrentReplay?.HasFooter == true && CurrentReplay?.HasCommentator == true && (RA3Directory != null) && File.Exists(RA3Exe);
|
||||||
|
|
||||||
|
public bool ReplayDamaged => CurrentReplay?.HasFooter == false;
|
||||||
|
|
||||||
|
public Replay? CurrentReplay
|
||||||
{
|
{
|
||||||
get { return _replayDetails; }
|
get => _currentReplay;
|
||||||
set { _replayDetails = value; NotifyPropertyChanged(_replayDetails); }
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ReplaySelected => _currentReplay != null;
|
|
||||||
|
|
||||||
public bool ReplayPlayable => _currentReplay?.HasFooter == true && _currentReplay?.HasCommentator == true && (RA3Directory != null) && File.Exists(RA3Exe);
|
|
||||||
|
|
||||||
public bool ReplayDamaged => _currentReplay?.HasFooter == false;
|
|
||||||
|
|
||||||
public Replay CurrentReplay
|
|
||||||
{
|
|
||||||
get { return _currentReplay; }
|
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_currentReplay = value;
|
SetAndNotifyPropertyChanged(ref _currentReplay, value);
|
||||||
NotifyPropertyChanged(_currentReplay);
|
|
||||||
ReplayDetails = "";
|
ReplayDetails = "";
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ReplayPlayable"));
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ReplayPlayable)));
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ReplaySelected"));
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ReplaySelected)));
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ReplayDamaged"));
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ReplayDamaged)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private volatile string _replayFolderPath;
|
private string _replayFolderPath = null!;
|
||||||
private volatile string _replayDetails;
|
private string? _replayDetails;
|
||||||
private volatile string _replayFilterString;
|
private Replay? _currentReplay;
|
||||||
private volatile Replay _currentReplay;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -107,360 +100,287 @@ namespace AnotherReplayReader
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class MainWindow : Window
|
public partial class MainWindow : Window
|
||||||
{
|
{
|
||||||
private MainWindowProperties _properties = new MainWindowProperties();
|
private readonly TaskQueue _taskQueue;
|
||||||
private volatile List<Replay> _replayList;
|
private readonly MainWindowProperties _properties = new();
|
||||||
private Cache _cache = new Cache();
|
private readonly Cache _cache = new();
|
||||||
private PlayerIdentity _playerIdentity;
|
private readonly BigMinimapCache _minimapCache;
|
||||||
private BigMinimapCache _minimapCache;
|
private readonly MinimapReader _minimapReader;
|
||||||
private MinimapReader _minimapReader;
|
private readonly CancelManager _cancelLoadReplays = new();
|
||||||
private CancellationTokenSource _loadReplaysToken;
|
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()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
|
_taskQueue = new(Dispatcher);
|
||||||
|
_minimapCache = new BigMinimapCache(_properties.RA3Directory);
|
||||||
|
_minimapReader = new MinimapReader(_minimapCache, _properties.CustomMapsDirectory, _properties.ModsDirectory);
|
||||||
|
_replayList = new();
|
||||||
|
|
||||||
DataContext = _properties;
|
DataContext = _properties;
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
Closing += (sender, eventArgs) =>
|
||||||
var handling = new bool[1] { false };
|
|
||||||
Application.Current.Dispatcher.UnhandledException += (sender, eventArgs) =>
|
|
||||||
{
|
{
|
||||||
if (handling == null || handling[0] == true)
|
_cache.Save().Wait();
|
||||||
{
|
Application.Current.Shutdown();
|
||||||
return;
|
|
||||||
}
|
|
||||||
handling[0] = true;
|
|
||||||
Dispatcher.Invoke(() => MessageBox.Show($"错误:\r\n{eventArgs.Exception}"));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Closing += ((sender, eventArgs) => _cache.Save());
|
|
||||||
|
|
||||||
_playerIdentity = new PlayerIdentity(_cache);
|
|
||||||
_minimapCache = new BigMinimapCache(_cache, _properties.RA3Directory);
|
|
||||||
_minimapReader = new MinimapReader(_minimapCache, _properties.RA3Directory, _properties.CustomMapsDirectory, _properties.ModsDirectory);
|
|
||||||
|
|
||||||
LoadReplays();
|
|
||||||
_ = AutoSaveReplays();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task AutoSaveReplays()
|
private async void OnMainWindowLoaded(object sender, EventArgs eventArgs)
|
||||||
{
|
{
|
||||||
const string ourPrefix = "自动保存";
|
Debug.Initialize();
|
||||||
var errorMessageCount = 0;
|
await _cache.Initialization;
|
||||||
// filename and last write time
|
ReplayAutoSaver.SpawnAutoSaveReplaysTask(_properties.RA3ReplayFolderPath);
|
||||||
var previousFiles = new Dictionary<string, DateTime>(StringComparer.OrdinalIgnoreCase);
|
var token = _cancelLoadReplays.ResetAndGetToken(CancellationToken.None);
|
||||||
// filename and file size
|
_ = _taskQueue.Enqueue(() => LoadReplays(null, token), token);
|
||||||
var lastReplays = new Dictionary<string, long>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
while (true)
|
const string permissionKey = "questionAsked";
|
||||||
|
if (_cache.GetOrDefault(permissionKey, false) is not true)
|
||||||
{
|
{
|
||||||
try
|
_cache.Set(permissionKey, true);
|
||||||
|
var sb = new StringWriter();
|
||||||
|
sb.WriteLine("要不要自动检查更新呢?");
|
||||||
|
sb.WriteLine("之后也可以在“关于”窗口里,设置自动更新的选项");
|
||||||
|
var choice = MessageBox.Show(this, sb.ToString(), App.Name, MessageBoxButton.YesNo);
|
||||||
|
_cache.Set(UpdateChecker.CheckForUpdatesKey, choice is MessageBoxResult.Yes);
|
||||||
|
await _cache.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = UpdateChecker.CheckForUpdates(_cache).ContinueWith(t => Dispatcher.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
var updateData = t.Result;
|
||||||
|
if (updateData.IsNewVersion())
|
||||||
{
|
{
|
||||||
var changed = (from fileName in Directory.GetFiles(_properties.RA3ReplayFolderPath, "*.RA3Replay")
|
var about = new About(_cache, updateData)
|
||||||
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;
|
Owner = this
|
||||||
|
};
|
||||||
|
about.ShowDialog();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadReplays(string? nextSelected, CancellationToken cancelToken)
|
||||||
|
{
|
||||||
|
if (!IsLoaded)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cancelToken.ThrowIfCancellationRequested();
|
||||||
|
var filterToken = _cancelFilterReplays.ResetAndGetToken(cancelToken);
|
||||||
|
_cancelDisplayReplays.Reset(_cancelFilterReplays.Token);
|
||||||
|
|
||||||
|
_properties.CurrentReplay = null;
|
||||||
|
_image.Source = null;
|
||||||
|
if (_dataGrid.Items.Count > 0)
|
||||||
|
{
|
||||||
|
_dataGrid.ItemsSource = Array.Empty<Replay>();
|
||||||
|
_dataGrid.Items.Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelToken.ThrowIfCancellationRequested();
|
||||||
|
_replayList = new();
|
||||||
|
|
||||||
|
var path = _properties.ReplayFolderPath;
|
||||||
|
if (!Directory.Exists(path))
|
||||||
|
{
|
||||||
|
await FilterReplays("这个文件夹并不存在。", nextSelected, filterToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await Task.Run(() =>
|
||||||
|
{
|
||||||
|
var list = new List<Replay>();
|
||||||
|
var clock = new Stopwatch();
|
||||||
|
clock.Start();
|
||||||
|
foreach (var replayPath in Directory.EnumerateFiles(path, "*.RA3Replay"))
|
||||||
|
{
|
||||||
|
cancelToken.ThrowIfCancellationRequested();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var replay = new Replay(replayPath);
|
||||||
|
list.Add(replay);
|
||||||
}
|
}
|
||||||
|
catch (Exception exception)
|
||||||
var replays = changed.Select(info =>
|
|
||||||
{
|
{
|
||||||
Debug.Instance.DebugMessage += $"正在尝试检测已更改的文件:{info.FullName}\r\n";
|
Debug.Instance.DebugMessage += $"Uncaught exception when loading replay list: \r\n{exception}\r\n";
|
||||||
try
|
continue;
|
||||||
{
|
|
||||||
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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (clock.ElapsedMilliseconds > 300)
|
||||||
foreach (var kv in toBeChecked)
|
|
||||||
{
|
{
|
||||||
Debug.Instance.DebugMessage += $"正在检测录像更改:{kv.Key}\r\n";
|
var text = $"正在加载录像列表,请稍候… 已加载 {list.Count} 个录像";
|
||||||
var replay = kv.Value;
|
Dispatcher.Invoke(() => _properties.ReplayDetails = text);
|
||||||
if (lastReplays.TryGetValue(kv.Key, out var fileSize))
|
clock.Restart();
|
||||||
{
|
|
||||||
if (fileSize == replay.Size)
|
|
||||||
{
|
|
||||||
// skip if size is not changed
|
|
||||||
Debug.Instance.DebugMessage += $"已跳过未更改的录像:{kv.Key}\r\n";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Debug.Instance.DebugMessage += $"将会自动保存已更改的录像:{kv.Key}\r\n";
|
|
||||||
lastReplays[kv.Key] = replay.Size;
|
|
||||||
|
|
||||||
var date = replay.Date;
|
|
||||||
|
|
||||||
var playerString = $"{replay.NumberOfPlayingPlayers}名玩家";
|
|
||||||
if (replay.NumberOfPlayingPlayers <= 2)
|
|
||||||
{
|
|
||||||
var playingPlayers = from player in replay.Players
|
|
||||||
let faction = ModData.GetFaction(replay.Mod, player.FactionID)
|
|
||||||
where faction.Kind != FactionKind.Observer
|
|
||||||
select $"{player.PlayerName}({faction.Name})";
|
|
||||||
playerString = playingPlayers.Aggregate(string.Empty, (x, y) => x + y);
|
|
||||||
}
|
|
||||||
|
|
||||||
var dateString = $"{date.Year}{date.Month:D2}{date.Day:D2}_{date.Hour:D2}{date.Minute:D2}{date.Second:D2}";
|
|
||||||
var destinationPath = Path.Combine(_properties.RA3ReplayFolderPath, $"{ourPrefix}-{playerString}{dateString}.RA3Replay");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
File.Copy(replay.Path, destinationPath, true);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
throw new Exception($"复制文件({replay.Path} -> {destinationPath})失败:{e.Message}", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
return new ReplayPinyinList(list.ToImmutableArray());
|
||||||
|
}, cancelToken);
|
||||||
|
cancelToken.ThrowIfCancellationRequested();
|
||||||
|
_replayList = result;
|
||||||
|
await FilterReplays(string.Empty, nextSelected, filterToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task FilterReplays(string message, string? nextSelected, CancellationToken cancelToken)
|
||||||
|
{
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
var errorString = $"自动保存录像时出现错误:\r\n{e}\r\n";
|
_dataGrid.ItemsSource = Array.Empty<Replay>();
|
||||||
Debug.Instance.DebugMessage += errorString;
|
_dataGrid.Items.Refresh();
|
||||||
_ = Dispatcher.InvokeAsync(() =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (Interlocked.Increment(ref errorMessageCount) == 1)
|
|
||||||
{
|
|
||||||
MessageBox.Show(errorString);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
Interlocked.Decrement(ref errorMessageCount);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.Delay(10 * 1000);
|
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 async void LoadReplays(string nextSelected = null)
|
private async Task DisplayReplayDetail(Replay replay, string replayDetails, CancellationToken cancelToken)
|
||||||
{
|
{
|
||||||
const string loadingString = "正在加载录像列表,请稍候";
|
if (!IsLoaded)
|
||||||
|
|
||||||
Dispatcher.Invoke(() =>
|
|
||||||
{
|
{
|
||||||
if (_image != null)
|
return;
|
||||||
{
|
}
|
||||||
_image.Source = null;
|
cancelToken.ThrowIfCancellationRequested();
|
||||||
}
|
_properties.CurrentReplay = null;
|
||||||
|
_image.Source = null;
|
||||||
|
_properties.ReplayDetails = replayDetails;
|
||||||
|
|
||||||
if (_dataGrid != null)
|
// 开始获取小地图
|
||||||
{
|
var mapPath = replay.MapPath;
|
||||||
_dataGrid.Items.Clear();
|
var minimapTask = _minimapReader.TryReadTargaAsync(replay);
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
_loadReplaysToken?.Cancel();
|
// 解析录像内容
|
||||||
_loadReplaysToken = new CancellationTokenSource();
|
try
|
||||||
|
|
||||||
var cancelToken = _loadReplaysToken.Token;
|
|
||||||
var path = _properties.ReplayFolderPath;
|
|
||||||
var task = Task.Run(async () =>
|
|
||||||
{
|
{
|
||||||
var messages = "";
|
replay = await Task.Run(() => new Replay(replay.Path, true));
|
||||||
var replayList = new List<Replay>();
|
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 (!Directory.Exists(path))
|
// 假如小地图变了(这……),那么重新加载小地图
|
||||||
|
if (replay.MapPath != mapPath)
|
||||||
|
{
|
||||||
|
minimapTask.Forget();
|
||||||
|
minimapTask = _minimapReader.TryReadTargaAsync(replay);
|
||||||
|
}
|
||||||
|
var newDetails = replay.GetDetails();
|
||||||
|
if (_properties.ReplayDetails != newDetails)
|
||||||
|
{
|
||||||
|
if (_replayList.Replays.FindIndex(r => r.PathEquals(replay)) is int index)
|
||||||
{
|
{
|
||||||
messages = "这个文件夹并不存在。";
|
_replayList = _replayList.SetItem(index, replay.CloneHeader());
|
||||||
|
var token = _cancelFilterReplays.ResetAndGetToken(_cancelLoadReplays.Token);
|
||||||
|
await FilterReplays(newDetails, replay.Path, token);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else
|
}
|
||||||
{
|
_properties.CurrentReplay = replay;
|
||||||
var replays = Directory.EnumerateFiles(path, "*.RA3Replay");
|
_properties.ReplayDetails = newDetails;
|
||||||
foreach (var replayPath in replays)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var replay = await Task.Run(() => new Replay(replayPath));
|
|
||||||
replayList.Add(replay);
|
|
||||||
_properties.ReplayDetails = loadingString + $"\n已加载 {replayList.Count} 个录像";
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
Debug.Instance.DebugMessage += $"Uncaught exception when loading replay list: \r\n{exception}\r\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelToken.ThrowIfCancellationRequested();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new { Replays = replayList, Messages = messages };
|
|
||||||
});
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await task;
|
var newSource = await minimapTask;
|
||||||
_replayList = result.Replays;
|
|
||||||
cancelToken.ThrowIfCancellationRequested();
|
cancelToken.ThrowIfCancellationRequested();
|
||||||
DisplayReplays(result.Messages, nextSelected);
|
_image.Source = newSource;
|
||||||
|
/* _image.Width = source.Width;
|
||||||
|
_image.Height = source.Height; */
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) { }
|
catch (Exception e) when (e is not OperationCanceledException)
|
||||||
}
|
|
||||||
|
|
||||||
private void DisplayReplays(string message = null, string nextSelected = null)
|
|
||||||
{
|
|
||||||
var filtered = _replayList;
|
|
||||||
Dispatcher.Invoke(() =>
|
|
||||||
{
|
{
|
||||||
_properties.CurrentReplay = null;
|
Debug.Instance.DebugMessage += $"Uncaught exception when loading minimap: \r\n{e}\r\n";
|
||||||
_dataGrid.Items.Clear();
|
}
|
||||||
filtered.ForEach(x => _dataGrid.Items.Add(x));
|
|
||||||
_properties.ReplayDetails = message;
|
|
||||||
|
|
||||||
if (nextSelected != null)
|
|
||||||
{
|
|
||||||
for (var i = 0; i < _dataGrid.Items.Count; ++i)
|
|
||||||
{
|
|
||||||
var replay = _dataGrid.Items[i] as Replay;
|
|
||||||
if (replay == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (replay.Path.Equals(nextSelected, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
_dataGrid.SelectedIndex = i;
|
|
||||||
OnReplaySelectionChanged(null, null);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnReplayFolderPathBoxTextChanged(object sender, EventArgs e)
|
private async void OnReplayFolderPathBoxTextChanged(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
LoadReplays();
|
var token = _cancelLoadReplays.ResetAndGetToken(CancellationToken.None);
|
||||||
|
await _taskQueue.Enqueue(() => LoadReplays(null, token), token);
|
||||||
|
var text = _replayFolderPathBox.Text;
|
||||||
|
const string assemblyMagic = "!DreamSign";
|
||||||
|
const string jsonMagic = "!FantasySeal";
|
||||||
|
switch (_replayFolderPathBox.Text)
|
||||||
|
{
|
||||||
|
case "!SpellCard":
|
||||||
|
_replayDetailsBox.Text = $"{assemblyMagic}\r\n";
|
||||||
|
_replayDetailsBox.Text += $"{jsonMagic}\r\n";
|
||||||
|
break;
|
||||||
|
case assemblyMagic:
|
||||||
|
break;
|
||||||
|
case jsonMagic:
|
||||||
|
UpdateChecker.Sign();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnReplaySelectionChanged(object sender, EventArgs e)
|
private async void OnReplaySelectionChanged(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
_properties.CurrentReplay = _dataGrid.SelectedItem as Replay;
|
if (_dataGrid.SelectedItem is not Replay replay)
|
||||||
if (_properties.CurrentReplay == null)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Dispatcher.Invoke(() => { _image.Source = null; });
|
var token = _cancelDisplayReplays.ResetAndGetToken(_cancelFilterReplays.Token);
|
||||||
|
_properties.ReplayDetails = replay.GetDetails();
|
||||||
string GetSizeString(double size)
|
await _taskQueue.Enqueue(() => DisplayReplayDetail(replay, _properties.ReplayDetails, token), token);
|
||||||
{
|
|
||||||
if (size > 1024 * 1024)
|
|
||||||
{
|
|
||||||
return $"{Math.Round(size / (1024 * 1024), 2)}MB";
|
|
||||||
}
|
|
||||||
return $"{Math.Round(size / 1024)}KB";
|
|
||||||
}
|
|
||||||
|
|
||||||
const string formatA = "文件名:{0}\n大小:{1}\n";
|
|
||||||
const string formatB = "地图:{0}\n日期:{1}\n长度:{2}\n";
|
|
||||||
const string formatC = "录像类别:{0}\n这个文件是{1}保存的\n";
|
|
||||||
const string playerListTitle = "玩家列表:\n";
|
|
||||||
var replay = _properties.CurrentReplay;
|
|
||||||
var sizeString = GetSizeString(replay.Size);
|
|
||||||
var lengthString = "录像已损坏,请先修复录像";
|
|
||||||
if (replay.HasFooter)
|
|
||||||
{
|
|
||||||
lengthString = $"{replay.Length}";
|
|
||||||
}
|
|
||||||
|
|
||||||
var replaySaver = "[无法获取保存录像的玩家]";
|
|
||||||
try
|
|
||||||
{
|
|
||||||
replaySaver = replay.ReplaySaver.PlayerName;
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
|
|
||||||
_properties.ReplayDetails = string.Format(formatA, replay.FileName, sizeString);
|
|
||||||
_properties.ReplayDetails += string.Format(formatB, replay.MapName, replay.Date, lengthString);
|
|
||||||
_properties.ReplayDetails += string.Format(formatC, replay.TypeString, replaySaver);
|
|
||||||
_properties.ReplayDetails += playerListTitle;
|
|
||||||
foreach (var player in replay.Players)
|
|
||||||
{
|
|
||||||
if (player == replay.Players.Last() && player.PlayerName.Equals("post Commentator"))
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
var factionName = ModData.GetFaction(replay.Mod, player.FactionID).Name;
|
|
||||||
var realName = replay.Type == ReplayType.Lan ? _playerIdentity.QueryRealName(player.PlayerIP) : string.Empty;
|
|
||||||
_properties.ReplayDetails += $"{player.PlayerName + realName},{factionName}\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var mapPath = replay.MapPath.TrimEnd('/');
|
|
||||||
var mapName = mapPath.Substring(mapPath.LastIndexOf('/') + 1);
|
|
||||||
var minimapPath = $"{mapPath}/{mapName}_art.tga";
|
|
||||||
Dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
var source = _minimapReader.TryReadTarga(minimapPath, replay.Mod);
|
|
||||||
_image.Source = source;
|
|
||||||
/*_image.Width = source.Width;
|
|
||||||
_image.Height = source.Height;*/
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
Debug.Instance.DebugMessage += $"Uncaught exception when loading minimap: \r\n{exception}\r\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
replay.ParseBody();
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
Debug.Instance.DebugMessage += $"Uncaught exception when loading replay body: \r\n{exception}\r\n";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnAboutButton_Click(object sender, RoutedEventArgs e)
|
private void OnAboutButtonClick(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var aboutWindow = new About();
|
var aboutWindow = new About(_cache)
|
||||||
|
{
|
||||||
|
Owner = this
|
||||||
|
};
|
||||||
aboutWindow.ShowDialog();
|
aboutWindow.ShowDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnBrowseButton_Click(object sender, RoutedEventArgs e)
|
private void OnBrowseButtonClick(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -470,13 +390,12 @@ namespace AnotherReplayReader
|
|||||||
InitialDirectory = _properties.ReplayFolderPath,
|
InitialDirectory = _properties.ReplayFolderPath,
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = openFileDialog.ShowDialog();
|
var result = openFileDialog.ShowDialog(this);
|
||||||
if (result == true)
|
if (result == true)
|
||||||
{
|
{
|
||||||
var fileName = openFileDialog.FileName;
|
var fileName = openFileDialog.FileName;
|
||||||
var directoryName = Path.GetDirectoryName(fileName);
|
var directoryName = Path.GetDirectoryName(fileName);
|
||||||
_properties.ReplayFolderPath = directoryName;
|
_properties.ReplayFolderPath = directoryName;
|
||||||
LoadReplays(fileName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
@ -485,29 +404,28 @@ namespace AnotherReplayReader
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDetailsButton_Click(object sender, RoutedEventArgs e)
|
private void OnDetailsButtonClick(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var detailsWindow = new APM(_properties.CurrentReplay, _playerIdentity);
|
var detailsWindow = new ApmWindow(_properties.CurrentReplay!)
|
||||||
|
{
|
||||||
|
Owner = this
|
||||||
|
};
|
||||||
detailsWindow.ShowDialog();
|
detailsWindow.ShowDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDebugButton_Click(object sender, RoutedEventArgs e)
|
private void OnDebugButtonClick(object sender, RoutedEventArgs e) => Debug.ShowDialog();
|
||||||
{
|
|
||||||
var debug = new Debug();
|
|
||||||
debug.ShowDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPlayReplayButton_Click(object sender, RoutedEventArgs e)
|
private void OnPlayReplayButtonClick(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
Process.Start(_properties.RA3Exe, $" -replayGame \"{_properties.CurrentReplay}\" ");
|
Process.Start(_properties.RA3Exe, $" -replayGame \"{_properties.CurrentReplay}\" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnFixReplayButton_Click(object sender, RoutedEventArgs e)
|
private async void OnFixReplayButtonClick(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
|
var replay = _properties.CurrentReplay ?? throw new InvalidOperationException("Trying to fix a null replay");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var replay = _properties.CurrentReplay;
|
|
||||||
|
|
||||||
var saveFileDialog = new SaveFileDialog
|
var saveFileDialog = new SaveFileDialog
|
||||||
{
|
{
|
||||||
Filter = "红警3录像文件 (*.RA3Replay)|*.RA3Replay|所有文件 (*.*)|*.*",
|
Filter = "红警3录像文件 (*.RA3Replay)|*.RA3Replay|所有文件 (*.*)|*.*",
|
||||||
@ -519,19 +437,31 @@ namespace AnotherReplayReader
|
|||||||
var result = saveFileDialog.ShowDialog(this);
|
var result = saveFileDialog.ShowDialog(this);
|
||||||
if (result == true)
|
if (result == true)
|
||||||
{
|
{
|
||||||
using (var file = saveFileDialog.OpenFile())
|
using var file = saveFileDialog.OpenFile();
|
||||||
using (var writer = new BinaryWriter(file))
|
using var writer = new BinaryWriter(file);
|
||||||
{
|
writer.Write(replay);
|
||||||
writer.WriteReplay(replay);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"无法修复录像:\r\n{exception}");
|
MessageBox.Show(this, $"无法修复录像:\r\n{exception}");
|
||||||
}
|
}
|
||||||
|
|
||||||
LoadReplays();
|
var token = _cancelLoadReplays.ResetAndGetToken(CancellationToken.None);
|
||||||
|
await _taskQueue.Enqueue(() => LoadReplays(replay.Path, token), token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnReplayFilterBoxTextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
|
||||||
|
{
|
||||||
|
_filterStrings = _replayFilterBox.Text
|
||||||
|
.Split(',', ' ', ',')
|
||||||
|
.Select(x => x.ToPinyin())
|
||||||
|
.Where(x => !string.IsNullOrEmpty(x))
|
||||||
|
.ToImmutableArray()!;
|
||||||
|
var currentReplayPath = _properties?.CurrentReplay?.Path;
|
||||||
|
|
||||||
|
var token = _cancelFilterReplays.ResetAndGetToken(_cancelLoadReplays.Token);
|
||||||
|
await _taskQueue.Enqueue(() => FilterReplays(string.Empty, currentReplayPath, token), token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
using OpenSage.FileFormats.Big;
|
using TechnologyAssembler.Core.IO;
|
||||||
using Pfim;
|
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
namespace AnotherReplayReader
|
||||||
{
|
{
|
||||||
@ -24,104 +24,93 @@ namespace AnotherReplayReader
|
|||||||
};
|
};
|
||||||
|
|
||||||
private readonly BigMinimapCache _cache;
|
private readonly BigMinimapCache _cache;
|
||||||
private readonly string _ra3InstallPath;
|
|
||||||
private readonly string _mapFolderPath;
|
private readonly string _mapFolderPath;
|
||||||
private readonly string _modFolderPath;
|
private readonly string _modFolderPath;
|
||||||
|
|
||||||
public MinimapReader(BigMinimapCache cache, string ra3InstallPath, string mapFolderPath, string modFolderPath)
|
public MinimapReader(BigMinimapCache cache, string mapFolderPath, string modFolderPath)
|
||||||
{
|
{
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
_ra3InstallPath = ra3InstallPath;
|
|
||||||
_mapFolderPath = mapFolderPath;
|
_mapFolderPath = mapFolderPath;
|
||||||
_modFolderPath = modFolderPath;
|
_modFolderPath = modFolderPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BitmapSource TryReadTarga(string path, Mod mod, double dpiX = 96.0, double dpiY = 96.0)
|
public Task<BitmapSource?> TryReadTargaAsync(Replay replay, double dpiX = 96.0, double dpiY = 96.0)
|
||||||
{
|
{
|
||||||
using (var targa = TryGetTarga(path, mod))
|
var mapPath = replay.MapPath.TrimEnd('/');
|
||||||
{
|
var mapName = mapPath.Substring(mapPath.LastIndexOf('/') + 1);
|
||||||
if(targa == null)
|
var minimapPath = $"{mapPath}/{mapName}_art.tga";
|
||||||
{
|
return Task.Run(() => TryReadTarga(minimapPath, replay.Mod, dpiX, dpiY));
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
try
|
public BitmapSource? TryReadTarga(string path, Mod mod, double dpiX = 96.0, double dpiY = 96.0)
|
||||||
{
|
{
|
||||||
return BitmapSource.Create(targa.Width, targa.Height, dpiX, dpiY, FormatMapper[targa.Format], null, targa.Data, targa.Stride);
|
using var targa = TryGetTarga(path, mod);
|
||||||
}
|
if (targa == null)
|
||||||
catch (Exception exception)
|
{
|
||||||
{
|
return null;
|
||||||
Debug.Instance.DebugMessage += $"Exception creating BitmapSource from minimap:\r\n {exception}\r\n";
|
}
|
||||||
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/";
|
const string customMapPrefix = "data/maps/internal/";
|
||||||
if (Directory.Exists(_mapFolderPath) && path.StartsWith(customMapPrefix))
|
if (Directory.Exists(_mapFolderPath) && path.StartsWith(customMapPrefix))
|
||||||
{
|
{
|
||||||
var minimapPath = Path.Combine(_mapFolderPath, path.Substring(customMapPrefix.Length));
|
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))
|
if (File.Exists(minimapPath))
|
||||||
{
|
{
|
||||||
return Targa.Create(File.ReadAllBytes(minimapPath), new PfimConfig());
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
106
ModData.cs
106
ModData.cs
@ -1,8 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
namespace AnotherReplayReader
|
||||||
{
|
{
|
||||||
@ -42,13 +39,13 @@ namespace AnotherReplayReader
|
|||||||
|
|
||||||
public int CompareTo(object other)
|
public int CompareTo(object other)
|
||||||
{
|
{
|
||||||
if(!(other is Mod))
|
if (!(other is Mod))
|
||||||
{
|
{
|
||||||
return GetType().FullName.CompareTo(other.GetType().FullName);
|
return GetType().FullName.CompareTo(other.GetType().FullName);
|
||||||
}
|
}
|
||||||
|
|
||||||
var otherMod = (Mod)other;
|
var otherMod = (Mod)other;
|
||||||
if(IsRA3 != otherMod.IsRA3)
|
if (IsRA3 != otherMod.IsRA3)
|
||||||
{
|
{
|
||||||
if (IsRA3)
|
if (IsRA3)
|
||||||
{
|
{
|
||||||
@ -84,20 +81,13 @@ namespace AnotherReplayReader
|
|||||||
|
|
||||||
internal static class ModData
|
internal static class ModData
|
||||||
{
|
{
|
||||||
private static readonly IReadOnlyDictionary<int, Faction> _ra3Factions;
|
private static readonly Faction _unknown = new(FactionKind.Unknown, "未知阵营");
|
||||||
private static readonly IReadOnlyDictionary<int, Faction> _ra3ARFactions;
|
private static readonly IReadOnlyDictionary<string, IReadOnlyDictionary<int, Faction>> _factions;
|
||||||
private static readonly IReadOnlyDictionary<int, Faction> _ra3CoronaFactions;
|
|
||||||
private static readonly IReadOnlyDictionary<int, Faction> _ra3DawnFactions;
|
|
||||||
private static readonly IReadOnlyDictionary<int, Faction> _ra3INSFactions;
|
|
||||||
private static readonly IReadOnlyDictionary<int, Faction> _ra3FSFactions;
|
|
||||||
private static readonly IReadOnlyDictionary<int, Faction> _ra3EisenreichFactions;
|
|
||||||
private static readonly IReadOnlyDictionary<int, Faction> _ra3TNWFactions;
|
|
||||||
private static readonly IReadOnlyDictionary<int, Faction> _ra3WOPFactions;
|
|
||||||
private static readonly Faction _unknown;
|
|
||||||
|
|
||||||
static ModData()
|
static ModData()
|
||||||
{
|
{
|
||||||
_ra3Factions = new Dictionary<int, Faction>
|
var ra3Factions = new Dictionary<int, Faction>
|
||||||
{
|
{
|
||||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||||
@ -107,7 +97,7 @@ namespace AnotherReplayReader
|
|||||||
{ 7, new Faction(FactionKind.Player, "随机") },
|
{ 7, new Faction(FactionKind.Player, "随机") },
|
||||||
{ 8, new Faction(FactionKind.Player, "苏联") },
|
{ 8, new Faction(FactionKind.Player, "苏联") },
|
||||||
};
|
};
|
||||||
_ra3ARFactions = new Dictionary<int, Faction>
|
var arFactions = new Dictionary<int, Faction>
|
||||||
{
|
{
|
||||||
{ 1, new Faction(FactionKind.Player, "阳炎") },
|
{ 1, new Faction(FactionKind.Player, "阳炎") },
|
||||||
{ 2, new Faction(FactionKind.Player, "天琼") },
|
{ 2, new Faction(FactionKind.Player, "天琼") },
|
||||||
@ -123,7 +113,7 @@ namespace AnotherReplayReader
|
|||||||
{ 14, new Faction(FactionKind.Player, "涅墨西斯") },
|
{ 14, new Faction(FactionKind.Player, "涅墨西斯") },
|
||||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||||
};
|
};
|
||||||
_ra3CoronaFactions = new Dictionary<int, Faction>
|
var coronaFactions = new Dictionary<int, Faction>
|
||||||
{
|
{
|
||||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||||
@ -134,7 +124,7 @@ namespace AnotherReplayReader
|
|||||||
{ 8, new Faction(FactionKind.Player, "苏联") },
|
{ 8, new Faction(FactionKind.Player, "苏联") },
|
||||||
{ 9, new Faction(FactionKind.Player, "神州") },
|
{ 9, new Faction(FactionKind.Player, "神州") },
|
||||||
};
|
};
|
||||||
_ra3DawnFactions = new Dictionary<int, Faction>
|
var dawnFactions = new Dictionary<int, Faction>
|
||||||
{
|
{
|
||||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||||
@ -147,7 +137,7 @@ namespace AnotherReplayReader
|
|||||||
{ 11, new Faction(FactionKind.Player, "随机") },
|
{ 11, new Faction(FactionKind.Player, "随机") },
|
||||||
{ 12, new Faction(FactionKind.Player, "苏联") },
|
{ 12, new Faction(FactionKind.Player, "苏联") },
|
||||||
};
|
};
|
||||||
_ra3INSFactions = new Dictionary<int, Faction>
|
var insFactions = new Dictionary<int, Faction>
|
||||||
{
|
{
|
||||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||||
@ -157,7 +147,7 @@ namespace AnotherReplayReader
|
|||||||
{ 7, new Faction(FactionKind.Player, "随机") },
|
{ 7, new Faction(FactionKind.Player, "随机") },
|
||||||
{ 8, new Faction(FactionKind.Player, "苏联") },
|
{ 8, new Faction(FactionKind.Player, "苏联") },
|
||||||
};
|
};
|
||||||
_ra3FSFactions = new Dictionary<int, Faction>
|
var fsFactions = new Dictionary<int, Faction>
|
||||||
{
|
{
|
||||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||||
@ -167,7 +157,7 @@ namespace AnotherReplayReader
|
|||||||
{ 7, new Faction(FactionKind.Player, "随机") },
|
{ 7, new Faction(FactionKind.Player, "随机") },
|
||||||
{ 8, new Faction(FactionKind.Player, "苏联") },
|
{ 8, new Faction(FactionKind.Player, "苏联") },
|
||||||
};
|
};
|
||||||
_ra3EisenreichFactions = new Dictionary<int, Faction>
|
var eisenreichFactions = new Dictionary<int, Faction>
|
||||||
{
|
{
|
||||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||||
@ -177,7 +167,7 @@ namespace AnotherReplayReader
|
|||||||
{ 7, new Faction(FactionKind.Player, "随机") },
|
{ 7, new Faction(FactionKind.Player, "随机") },
|
||||||
{ 8, new Faction(FactionKind.Player, "苏联") },
|
{ 8, new Faction(FactionKind.Player, "苏联") },
|
||||||
};
|
};
|
||||||
_ra3TNWFactions = new Dictionary<int, Faction>
|
var tnwFactions = new Dictionary<int, Faction>
|
||||||
{
|
{
|
||||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||||
@ -187,7 +177,7 @@ namespace AnotherReplayReader
|
|||||||
{ 7, new Faction(FactionKind.Player, "随机") },
|
{ 7, new Faction(FactionKind.Player, "随机") },
|
||||||
{ 8, new Faction(FactionKind.Player, "苏联") },
|
{ 8, new Faction(FactionKind.Player, "苏联") },
|
||||||
};
|
};
|
||||||
_ra3WOPFactions = new Dictionary<int, Faction>
|
var wopFactions = new Dictionary<int, Faction>
|
||||||
{
|
{
|
||||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||||
@ -197,61 +187,31 @@ namespace AnotherReplayReader
|
|||||||
{ 7, new Faction(FactionKind.Player, "随机") },
|
{ 7, new Faction(FactionKind.Player, "随机") },
|
||||||
{ 8, new Faction(FactionKind.Player, "苏联") },
|
{ 8, new Faction(FactionKind.Player, "苏联") },
|
||||||
};
|
};
|
||||||
_unknown = new Faction(FactionKind.Unknown, "未知阵营");
|
_factions = new Dictionary<string, IReadOnlyDictionary<int, Faction>>(StringComparer.CurrentCultureIgnoreCase)
|
||||||
|
{
|
||||||
|
["RA3"] = ra3Factions,
|
||||||
|
["Armor Rush"] = arFactions,
|
||||||
|
["ART"] = arFactions,
|
||||||
|
["corona"] = coronaFactions,
|
||||||
|
["Dawn"] = dawnFactions,
|
||||||
|
["Insurrection"] = insFactions,
|
||||||
|
["1.12+FS"] = fsFactions,
|
||||||
|
["Eisenreich"] = eisenreichFactions,
|
||||||
|
["The New World"] = tnwFactions,
|
||||||
|
["War Of Powers"] = wopFactions
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Faction GetFaction(Mod mod, int factionID)
|
public static Faction GetFaction(Mod mod, int factionId)
|
||||||
{
|
{
|
||||||
new Faction(FactionKind.Player, mod.ModName + "-" + factionID);
|
if (_factions.TryGetValue(mod.ModName, out var table))
|
||||||
|
|
||||||
if
|
|
||||||
(mod.ModName.Equals("RA3"))
|
|
||||||
{
|
{
|
||||||
return _ra3Factions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
if (table.TryGetValue(factionId, out var result))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (mod.ModName.Equals("Armor Rush"))
|
|
||||||
{
|
|
||||||
return _ra3ARFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
|
||||||
}
|
|
||||||
if (mod.ModName.Equals("ART"))
|
|
||||||
{
|
|
||||||
return _ra3ARFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
|
||||||
}
|
|
||||||
if (mod.ModName.Equals("corona"))
|
|
||||||
{
|
|
||||||
return _ra3CoronaFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
|
||||||
}
|
|
||||||
if (mod.ModName.Equals("Dawn"))
|
|
||||||
{
|
|
||||||
return _ra3DawnFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
|
||||||
}
|
|
||||||
if (mod.ModName.Equals("Insurrection"))
|
|
||||||
{
|
|
||||||
return _ra3INSFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
|
||||||
}
|
|
||||||
if (mod.ModName.Equals("1.12+FS"))
|
|
||||||
{
|
|
||||||
return _ra3FSFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
|
||||||
}
|
|
||||||
if (mod.ModName.Equals("Eisenreich"))
|
|
||||||
{
|
|
||||||
return _ra3EisenreichFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
|
||||||
}
|
|
||||||
if (mod.ModName.Equals("The New World"))
|
|
||||||
{
|
|
||||||
return _ra3TNWFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
|
||||||
}
|
|
||||||
if (mod.ModName.Equals("War Of Powers"))
|
|
||||||
{
|
|
||||||
return _ra3WOPFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return _unknown;
|
return _unknown;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,159 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using System.Web;
|
|
||||||
using System.Web.Script.Serialization;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
|
||||||
{
|
|
||||||
internal sealed class IPAndPlayer
|
|
||||||
{
|
|
||||||
public static string SimpleIPToString(uint ip)
|
|
||||||
{
|
|
||||||
return $"{ip / 256 / 256 / 256}.{(ip / 256 / 256) % 256}.{(ip / 256) % 256}.{ip % 256}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public uint IP
|
|
||||||
{
|
|
||||||
get { return _ip; }
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_ip = value;
|
|
||||||
IPString = SimpleIPToString(_ip);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public string IPString { get; private set; }
|
|
||||||
public string ID { get; set; }
|
|
||||||
|
|
||||||
private uint _ip;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class PlayerIdentity
|
|
||||||
{
|
|
||||||
public bool IsUsable => IsListUsable(_list);
|
|
||||||
|
|
||||||
private Cache _cache;
|
|
||||||
private volatile IReadOnlyDictionary<uint, string> _list;
|
|
||||||
|
|
||||||
public PlayerIdentity(Cache cache)
|
|
||||||
{
|
|
||||||
_cache = cache;
|
|
||||||
Fetch();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var stored = _cache.GetOrDefault("pt", string.Empty);
|
|
||||||
if (string.IsNullOrWhiteSpace(stored))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var bytes = Convert.FromBase64String(stored);
|
|
||||||
var id = Encoding.UTF8.GetBytes(Auth.ID);
|
|
||||||
var data = Encoding.UTF8.GetString(bytes.Select((x, i) => (byte)(x ^ id[i % id.Length])).ToArray());
|
|
||||||
var serializer = new JavaScriptSerializer();
|
|
||||||
var cachedTable = serializer.Deserialize<List<IPAndPlayer>>(data);
|
|
||||||
if(cachedTable == null)
|
|
||||||
{
|
|
||||||
_list = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var converted = cachedTable.ToDictionary(x => x.IP, x => x.ID);
|
|
||||||
converted[0] = "【没有网络连接,正在使用上次保存的数据】";
|
|
||||||
|
|
||||||
_list = converted;
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool IsListUsable(IReadOnlyDictionary<uint, string> list)
|
|
||||||
{
|
|
||||||
return list != null && list.Count != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Fetch()
|
|
||||||
{
|
|
||||||
return Task.Run(() =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var key = HttpUtility.UrlEncode(Auth.GetKey());
|
|
||||||
var request = WebRequest.Create($"https://lanyi.altervista.org/playertable/playertable.php?do=getTable&key={key}");
|
|
||||||
|
|
||||||
using (var stream = request.GetResponse().GetResponseStream())
|
|
||||||
using (var reader = new StreamReader(stream))
|
|
||||||
{
|
|
||||||
var response = reader.ReadToEnd();
|
|
||||||
var serializer = new JavaScriptSerializer();
|
|
||||||
var temp = serializer.Deserialize<List<IPAndPlayer>>(response);
|
|
||||||
if(temp == null)
|
|
||||||
{
|
|
||||||
_list = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var converted = temp.ToDictionary(x => x.IP, x => x.ID);
|
|
||||||
_list = converted;
|
|
||||||
|
|
||||||
var bytes = Encoding.UTF8.GetBytes(serializer.Serialize(temp));
|
|
||||||
var id = Encoding.UTF8.GetBytes(Auth.ID);
|
|
||||||
var base64 = Convert.ToBase64String(bytes.Select((x, i) => (byte)(x ^ id[i % id.Length])).ToArray());
|
|
||||||
_cache.Set("pt", base64);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<IPAndPlayer> AsSortedList()
|
|
||||||
{
|
|
||||||
var list = _list;
|
|
||||||
|
|
||||||
if(!IsListUsable(list))
|
|
||||||
{
|
|
||||||
return new List<IPAndPlayer>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return _list.Select((kv) => new IPAndPlayer { IP = kv.Key, ID = kv.Value }).OrderBy(x => x.IP).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string QueryRealName(uint ip)
|
|
||||||
{
|
|
||||||
var list = _list;
|
|
||||||
|
|
||||||
if (!IsListUsable(list))
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(ip == 0)
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
var name = list.TryGetValue(ip, out var realName) ? realName : IPAndPlayer.SimpleIPToString(ip);
|
|
||||||
return $"({name})";
|
|
||||||
}
|
|
||||||
|
|
||||||
public string QueryRealNameAndIP(uint ip)
|
|
||||||
{
|
|
||||||
var list = _list;
|
|
||||||
|
|
||||||
if (!IsListUsable(list))
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ip == 0)
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
var name = list.TryGetValue(ip, out var realName) ? realName + "," : string.Empty;
|
|
||||||
return name + IPAndPlayer.SimpleIPToString(ip);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,18 +4,6 @@ using System.Runtime.CompilerServices;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
||||||
// 有关程序集的一般信息由以下
|
|
||||||
// 控制。更改这些特性值可修改
|
|
||||||
// 与程序集关联的信息。
|
|
||||||
[assembly: AssemblyTitle("AnotherReplayReader")]
|
|
||||||
[assembly: AssemblyDescription("")]
|
|
||||||
[assembly: AssemblyConfiguration("")]
|
|
||||||
[assembly: AssemblyCompany("")]
|
|
||||||
[assembly: AssemblyProduct("AnotherReplayReader")]
|
|
||||||
[assembly: AssemblyCopyright("Copyright © 2019")]
|
|
||||||
[assembly: AssemblyTrademark("")]
|
|
||||||
[assembly: AssemblyCulture("")]
|
|
||||||
|
|
||||||
// 将 ComVisible 设置为 false 会使此程序集中的类型
|
// 将 ComVisible 设置为 false 会使此程序集中的类型
|
||||||
//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
|
//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
|
||||||
//请将此类型的 ComVisible 特性设置为 true。
|
//请将此类型的 ComVisible 特性设置为 true。
|
||||||
@ -39,17 +27,3 @@ using System.Windows;
|
|||||||
//(未在页面中找到资源时使用,
|
//(未在页面中找到资源时使用,
|
||||||
//、应用程序或任何主题专用资源字典中找到时使用)
|
//、应用程序或任何主题专用资源字典中找到时使用)
|
||||||
)]
|
)]
|
||||||
|
|
||||||
|
|
||||||
// 程序集的版本信息由下列四个值组成:
|
|
||||||
//
|
|
||||||
// 主版本
|
|
||||||
// 次版本
|
|
||||||
// 生成号
|
|
||||||
// 修订号
|
|
||||||
//
|
|
||||||
// 可以指定所有值,也可以使用以下所示的 "*" 预置版本号和修订号
|
|
||||||
// 方法是按如下所示使用“*”: :
|
|
||||||
// [assembly: AssemblyVersion("1.0.*")]
|
|
||||||
[assembly: AssemblyVersion("0.0.3.0")]
|
|
||||||
[assembly: AssemblyFileVersion("0.0.3.0")]
|
|
||||||
|
431
Replay.cs
431
Replay.cs
@ -1,431 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
|
||||||
{
|
|
||||||
internal sealed class Player
|
|
||||||
{
|
|
||||||
public static readonly IReadOnlyDictionary<string, string> ComputerNames = new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{ "E", "简单" },
|
|
||||||
{ "M", "中等" },
|
|
||||||
{ "H", "困难" },
|
|
||||||
{ "B", "凶残" },
|
|
||||||
};
|
|
||||||
|
|
||||||
public string PlayerName { get; private set; }
|
|
||||||
public uint PlayerIP { get; private set; }
|
|
||||||
public string PlayerRealName { get; private set; }
|
|
||||||
public int FactionID { get; private set; }
|
|
||||||
public int Team { get; private set; }
|
|
||||||
|
|
||||||
public Player(string[] playerEntry)
|
|
||||||
{
|
|
||||||
var isComputer = playerEntry[0][0] == 'C';
|
|
||||||
|
|
||||||
PlayerName = playerEntry[0].Substring(1);
|
|
||||||
|
|
||||||
if (isComputer)
|
|
||||||
{
|
|
||||||
PlayerName = ComputerNames[PlayerName];
|
|
||||||
PlayerIP = 0;
|
|
||||||
FactionID = int.Parse(playerEntry[2]);
|
|
||||||
Team = int.Parse(playerEntry[4]);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
PlayerIP = uint.Parse(playerEntry[1], System.Globalization.NumberStyles.HexNumber);
|
|
||||||
FactionID = int.Parse(playerEntry[5]);
|
|
||||||
Team = int.Parse(playerEntry[7]);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal enum ReplayType
|
|
||||||
{
|
|
||||||
Skirmish,
|
|
||||||
Lan,
|
|
||||||
Online
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class ReplayChunk
|
|
||||||
{
|
|
||||||
public uint TimeCode { get; private set; }
|
|
||||||
public byte Type { get; private set; }
|
|
||||||
public byte[] Data { get; private set; }
|
|
||||||
|
|
||||||
public ReplayChunk(uint timeCode, BinaryReader reader)
|
|
||||||
{
|
|
||||||
TimeCode = timeCode; // reader.ReadUInt32();
|
|
||||||
Type = reader.ReadByte();
|
|
||||||
var chunkSize = reader.ReadInt32();
|
|
||||||
Data = reader.ReadBytes(chunkSize);
|
|
||||||
if(reader.ReadInt32() != 0)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException("Replay Chunk not ended with zero");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal enum ReplayFooterOption
|
|
||||||
{
|
|
||||||
SeekToFooter,
|
|
||||||
CurrentlyAtFooter,
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class ReplayFooter
|
|
||||||
{
|
|
||||||
public const uint Terminator = 0x7FFFFFFF;
|
|
||||||
public static readonly byte[] FooterString = Encoding.ASCII.GetBytes("RA3 REPLAY FOOTER");
|
|
||||||
public uint FinalTimeCode { get; private set; }
|
|
||||||
public byte[] Data { get; private set; }
|
|
||||||
|
|
||||||
public ReplayFooter(BinaryReader reader, ReplayFooterOption option)
|
|
||||||
{
|
|
||||||
var currentPosition = reader.BaseStream.Position;
|
|
||||||
reader.BaseStream.Seek(-4, SeekOrigin.End);
|
|
||||||
var footerLength = reader.ReadInt32();
|
|
||||||
|
|
||||||
if (option == ReplayFooterOption.SeekToFooter)
|
|
||||||
{
|
|
||||||
currentPosition = reader.BaseStream.Length - footerLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
reader.BaseStream.Seek(currentPosition, SeekOrigin.Begin);
|
|
||||||
var footer = reader.ReadBytes(footerLength);
|
|
||||||
|
|
||||||
if(footer.Length != footerLength || reader.BaseStream.Position != reader.BaseStream.Length)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException("Invalid footer");
|
|
||||||
}
|
|
||||||
|
|
||||||
using (var footerStream = new MemoryStream(footer))
|
|
||||||
using (var footerReader = new BinaryReader(footerStream))
|
|
||||||
{
|
|
||||||
var footerString = footerReader.ReadBytes(17);
|
|
||||||
if(!footerString.SequenceEqual(FooterString))
|
|
||||||
{
|
|
||||||
throw new InvalidDataException("Invalid footer, no footer string");
|
|
||||||
}
|
|
||||||
FinalTimeCode = footerReader.ReadUInt32();
|
|
||||||
Data = footerReader.ReadBytes(footer.Length - 25);
|
|
||||||
if(footerReader.ReadInt32() != footerLength)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ReplayFooter(uint finalTimeCode)
|
|
||||||
{
|
|
||||||
FinalTimeCode = finalTimeCode;
|
|
||||||
Data = new byte[] { 0x02, 0x1A, 0x00, 0x00, 0x00 };
|
|
||||||
}
|
|
||||||
|
|
||||||
List<float> TryGetKillDeathRatio()
|
|
||||||
{
|
|
||||||
if(Data.Length < 24)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ratios = new List<float>();
|
|
||||||
using (var stream = new MemoryStream(Data, Data.Length - 24, 24))
|
|
||||||
using (var reader = new BinaryReader(stream))
|
|
||||||
{
|
|
||||||
ratios.Add(reader.ReadSingle());
|
|
||||||
}
|
|
||||||
|
|
||||||
return ratios;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static class ReplayExtensions
|
|
||||||
{
|
|
||||||
public static string ReadUTF16String(this BinaryReader reader)
|
|
||||||
{
|
|
||||||
var currentBytes = new List<byte>();
|
|
||||||
byte[] lastTwoBytes = null;
|
|
||||||
while(true)
|
|
||||||
{
|
|
||||||
lastTwoBytes = reader.ReadBytes(2);
|
|
||||||
if (lastTwoBytes.Length != 2)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException();
|
|
||||||
}
|
|
||||||
if(lastTwoBytes.All(x => x == 0))
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
currentBytes.AddRange(lastTwoBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Encoding.Unicode.GetString(currentBytes.ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void WriteChunk(this BinaryWriter writer, ReplayChunk chunk)
|
|
||||||
{
|
|
||||||
writer.Write(chunk.TimeCode);
|
|
||||||
writer.Write(chunk.Type);
|
|
||||||
writer.Write(chunk.Data.Length);
|
|
||||||
writer.Write(chunk.Data);
|
|
||||||
writer.Write(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void WriteFooter(this BinaryWriter writer, ReplayFooter footer)
|
|
||||||
{
|
|
||||||
writer.Write(ReplayFooter.Terminator);
|
|
||||||
writer.Write(ReplayFooter.FooterString);
|
|
||||||
writer.Write(footer.FinalTimeCode);
|
|
||||||
writer.Write(footer.Data);
|
|
||||||
writer.Write(ReplayFooter.FooterString.Length + footer.Data.Length + 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void WriteReplay(this BinaryWriter writer, Replay replay)
|
|
||||||
{
|
|
||||||
writer.Write(replay.RawHeader);
|
|
||||||
|
|
||||||
var lastTimeCode = (uint)0;
|
|
||||||
foreach(var chunk in replay.Body)
|
|
||||||
{
|
|
||||||
lastTimeCode = chunk.TimeCode;
|
|
||||||
writer.WriteChunk(chunk);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (replay.HasFooter)
|
|
||||||
{
|
|
||||||
writer.WriteFooter(replay.Footer);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
writer.WriteFooter(new ReplayFooter(lastTimeCode));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class Replay
|
|
||||||
{
|
|
||||||
public static readonly byte[] HeaderMagic = Encoding.ASCII.GetBytes("RA3 REPLAY HEADER");
|
|
||||||
public static readonly Dictionary<ReplayType, string> TypeStrings = new Dictionary<ReplayType, string>()
|
|
||||||
{
|
|
||||||
{ ReplayType.Skirmish, "遭遇战录像" },
|
|
||||||
{ ReplayType.Lan, "局域网录像" },
|
|
||||||
{ ReplayType.Online, "官网录像" },
|
|
||||||
};
|
|
||||||
|
|
||||||
public string Path { get; private set; }
|
|
||||||
public string FileName => System.IO.Path.GetFileNameWithoutExtension(Path);
|
|
||||||
public DateTime Date { get; private set; }
|
|
||||||
public bool HasFooter => Footer != null;
|
|
||||||
public TimeSpan? Length { get; private set; }
|
|
||||||
public string MapName { get; private set; }
|
|
||||||
public string MapPath { get; private set; }
|
|
||||||
public IReadOnlyList<Player> Players => _players;
|
|
||||||
public int NumberOfPlayingPlayers { get; private set; }
|
|
||||||
public long Size { get; private set; }
|
|
||||||
public Mod Mod { get; private set; }
|
|
||||||
public ReplayType Type { get; private set; }
|
|
||||||
public string TypeString => TypeStrings[Type];
|
|
||||||
public bool HasCommentator { get; private set; }
|
|
||||||
public Player ReplaySaver => Players[_replaySaverIndex];
|
|
||||||
|
|
||||||
public byte[] RawHeader { get; private set; }
|
|
||||||
public IReadOnlyList<ReplayChunk> Body => _body;
|
|
||||||
public ReplayFooter Footer { get; private set; }
|
|
||||||
|
|
||||||
private List<Player> _players;
|
|
||||||
private byte _replaySaverIndex;
|
|
||||||
private long _headerSize;
|
|
||||||
private List<ReplayChunk> _body;
|
|
||||||
|
|
||||||
public Replay(string path)
|
|
||||||
{
|
|
||||||
using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
|
|
||||||
{
|
|
||||||
Parse(path, stream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Replay(string path, Stream stream)
|
|
||||||
{
|
|
||||||
Parse(path, stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Parse(string path, Stream stream)
|
|
||||||
{
|
|
||||||
Path = path;
|
|
||||||
|
|
||||||
using (var reader = new BinaryReader(stream))
|
|
||||||
{
|
|
||||||
Size = reader.BaseStream.Length;
|
|
||||||
var headerMagic = reader.ReadBytes(HeaderMagic.Length);
|
|
||||||
if(!headerMagic.SequenceEqual(HeaderMagic))
|
|
||||||
{
|
|
||||||
throw new InvalidDataException($"{Path} is not a replay, header is {BitConverter.ToString(headerMagic)}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var isSkirmish = reader.ReadByte() == 0x04;
|
|
||||||
reader.ReadBytes(4 * 4); // version and builds
|
|
||||||
reader.ReadBytes(2); // commentary flag, and padding zero byte
|
|
||||||
|
|
||||||
reader.ReadUTF16String(); // title
|
|
||||||
reader.ReadUTF16String(); // description
|
|
||||||
MapName = reader.ReadUTF16String(); // map name
|
|
||||||
reader.ReadUTF16String(); // map id
|
|
||||||
|
|
||||||
NumberOfPlayingPlayers = reader.ReadByte();
|
|
||||||
|
|
||||||
for(var i = 0; i <= NumberOfPlayingPlayers; ++i)
|
|
||||||
{
|
|
||||||
reader.ReadUInt32();
|
|
||||||
reader.ReadUTF16String(); // utf16 player name
|
|
||||||
if(!isSkirmish)
|
|
||||||
{
|
|
||||||
reader.ReadByte(); // team
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var offset = reader.ReadInt32();
|
|
||||||
var cnc3MagicLength = reader.ReadInt32();
|
|
||||||
_headerSize = reader.BaseStream.Position + offset;
|
|
||||||
reader.ReadBytes(cnc3MagicLength);
|
|
||||||
|
|
||||||
var modInfo = reader.ReadBytes(22);
|
|
||||||
Mod = new Mod(Encoding.UTF8.GetString(modInfo));
|
|
||||||
|
|
||||||
var timeStamp = reader.ReadUInt32();
|
|
||||||
Date = DateTimeOffset.FromUnixTimeSeconds(timeStamp).DateTime;
|
|
||||||
|
|
||||||
reader.ReadBytes(31);
|
|
||||||
var descriptionsLength = reader.ReadInt32();
|
|
||||||
var description = Encoding.UTF8.GetString(reader.ReadBytes(descriptionsLength));
|
|
||||||
|
|
||||||
var entries = null as Dictionary<string, string>;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
entries = description.Split(';').Where(x => !string.IsNullOrWhiteSpace(x)).ToDictionary(x => x.Split('=')[0], x => x.Split('=')[1]);
|
|
||||||
}
|
|
||||||
catch(Exception exception)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException($"Failed to parse string header of replay {Path}: \r\n{exception}");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_players = entries["S"].Split(':')
|
|
||||||
.TakeWhile(x => !string.IsNullOrWhiteSpace(x) && x[0] != 'X')
|
|
||||||
.Select(x => new Player(x.Split(',')))
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException($"Failed to parse playerdata from string header of replay {Path}: \r\n{exception}");
|
|
||||||
}
|
|
||||||
|
|
||||||
MapPath = entries["M"].Substring(3);
|
|
||||||
|
|
||||||
HasCommentator = !entries["PC"].Equals("-1");
|
|
||||||
|
|
||||||
var lanFlag = int.Parse(entries["GT"]) == 0;
|
|
||||||
if (lanFlag)
|
|
||||||
{
|
|
||||||
if(_players.First().PlayerIP == 0)
|
|
||||||
{
|
|
||||||
Type = ReplayType.Skirmish;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Type = ReplayType.Lan;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Type = ReplayType.Online;
|
|
||||||
}
|
|
||||||
|
|
||||||
_replaySaverIndex = reader.ReadByte();
|
|
||||||
|
|
||||||
reader.ReadBytes(8); // 8 bit paddings
|
|
||||||
var fileNameLength = reader.ReadInt32();
|
|
||||||
reader.ReadBytes(fileNameLength * 2);
|
|
||||||
reader.ReadBytes(16);
|
|
||||||
var verMagicLength = reader.ReadInt32();
|
|
||||||
reader.ReadBytes(verMagicLength);
|
|
||||||
reader.ReadBytes(85);
|
|
||||||
|
|
||||||
if(reader.BaseStream.Position != _headerSize)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException();
|
|
||||||
}
|
|
||||||
|
|
||||||
reader.BaseStream.Seek(-4, SeekOrigin.End);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Footer = new ReplayFooter(reader, ReplayFooterOption.SeekToFooter);
|
|
||||||
Length = TimeSpan.FromSeconds(Math.Round(Footer.FinalTimeCode / 15.0));
|
|
||||||
}
|
|
||||||
catch(Exception)
|
|
||||||
{
|
|
||||||
Length = null;
|
|
||||||
Footer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ParseBody()
|
|
||||||
{
|
|
||||||
_body = new List<ReplayChunk>();
|
|
||||||
|
|
||||||
using (var stream = new FileStream(Path, FileMode.Open))
|
|
||||||
using (var reader = new BinaryReader(stream))
|
|
||||||
{
|
|
||||||
RawHeader = reader.ReadBytes((int)_headerSize);
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
var timeCode = reader.ReadUInt32();
|
|
||||||
if (timeCode == ReplayFooter.Terminator)
|
|
||||||
{
|
|
||||||
Footer = new ReplayFooter(reader, ReplayFooterOption.CurrentlyAtFooter);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
_body.Add(new ReplayChunk(timeCode, reader));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Dictionary<byte, int[]> GetCommandCounts()
|
|
||||||
{
|
|
||||||
var playerCommands = new Dictionary<byte, int[]>();
|
|
||||||
|
|
||||||
foreach(var chunk in _body)
|
|
||||||
{
|
|
||||||
if(chunk.Type != 1)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach(var command in CommandChunk.Parse(chunk))
|
|
||||||
{
|
|
||||||
var commandCount = playerCommands.TryGetValue(command.CommandID, out var current) ? current : new int[Players.Count];
|
|
||||||
if(command.PlayerIndex >= commandCount.Length) // unknown or unparsable command?
|
|
||||||
{
|
|
||||||
commandCount = commandCount
|
|
||||||
.Concat(new int[command.PlayerIndex - commandCount.Length + 1])
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
commandCount[command.PlayerIndex] = commandCount[command.PlayerIndex] + 1;
|
|
||||||
playerCommands[command.CommandID] = commandCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return playerCommands;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
148
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
57
ReplayFile/CommandChunk.cs
Normal file
57
ReplayFile/CommandChunk.cs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.ReplayFile
|
||||||
|
{
|
||||||
|
internal sealed class CommandChunk
|
||||||
|
{
|
||||||
|
public byte CommandId { get; private set; }
|
||||||
|
public int PlayerIndex { get; private set; }
|
||||||
|
|
||||||
|
public static List<CommandChunk> Parse(ReplayChunk chunk)
|
||||||
|
{
|
||||||
|
if (chunk.Type != 1)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ManglePlayerId(byte id)
|
||||||
|
{
|
||||||
|
return id / 8 - 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var reader = chunk.GetReader();
|
||||||
|
if (reader.ReadByte() != 1)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException("Payload first byte not 1");
|
||||||
|
}
|
||||||
|
|
||||||
|
var list = new List<CommandChunk>();
|
||||||
|
var numberOfCommands = reader.ReadInt32();
|
||||||
|
for (var i = 0; i < numberOfCommands; ++i)
|
||||||
|
{
|
||||||
|
var 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"[玩家 {PlayerIndex},{RA3Commands.GetCommandName(CommandId)}]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
ReplayFile/Player.cs
Normal file
47
ReplayFile/Player.cs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
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 bool IsComputer { get; }
|
||||||
|
public string PlayerName { get; }
|
||||||
|
public uint PlayerIp { get; }
|
||||||
|
public int FactionId { get; }
|
||||||
|
public int Team { get; }
|
||||||
|
|
||||||
|
public Player(string[] playerEntry)
|
||||||
|
{
|
||||||
|
IsComputer = playerEntry[0][0] == 'C';
|
||||||
|
|
||||||
|
PlayerName = playerEntry[0].Substring(1);
|
||||||
|
|
||||||
|
if (IsComputer)
|
||||||
|
{
|
||||||
|
PlayerName = ComputerNames[PlayerName];
|
||||||
|
PlayerIp = 0;
|
||||||
|
FactionId = int.Parse(playerEntry[2]);
|
||||||
|
Team = int.Parse(playerEntry[4]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PlayerIp = uint.Parse(playerEntry[1], System.Globalization.NumberStyles.HexNumber);
|
||||||
|
FactionId = int.Parse(playerEntry[5]);
|
||||||
|
Team = int.Parse(playerEntry[7]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PlayerName.Length == 20)
|
||||||
|
{
|
||||||
|
PlayerName = Ra3.BattleNet.Database.Utils.ChineseEncoding.DecodeChineseFromBase64(PlayerName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
289
ReplayFile/RA3Commands.cs
Normal file
289
ReplayFile/RA3Commands.cs
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.ReplayFile
|
||||||
|
{
|
||||||
|
internal static class RA3Commands
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<byte, Action<BinaryReader>> _commandParser;
|
||||||
|
private static readonly Dictionary<byte, string> _commandNames;
|
||||||
|
|
||||||
|
public static ImmutableArray<byte> UnknownCommands { get; } = new byte[]
|
||||||
|
{
|
||||||
|
0x0F, // unk
|
||||||
|
0x5F, // unk
|
||||||
|
0x12, // unk
|
||||||
|
0x1B, // unk
|
||||||
|
0x48, // unk
|
||||||
|
0x52, // unk
|
||||||
|
0xFC, // unk
|
||||||
|
0xFD, // unk
|
||||||
|
}.ToImmutableArray();
|
||||||
|
|
||||||
|
public static ImmutableArray<byte> AutoCommands { get; } = new byte[]
|
||||||
|
{
|
||||||
|
0x01, // auto gen
|
||||||
|
0x21, // 3 seconds heartbeat
|
||||||
|
0x33, // uuid
|
||||||
|
0x34, // uuid
|
||||||
|
0x35, // player info
|
||||||
|
0x37, // indeterminate autogen
|
||||||
|
0x47, // 5th frame auto gen
|
||||||
|
0xF6, // auto, maybe from barrack deploy
|
||||||
|
0xF9, // auto, maybe unit from structures
|
||||||
|
}.ToImmutableArray();
|
||||||
|
|
||||||
|
static RA3Commands()
|
||||||
|
{
|
||||||
|
Action<BinaryReader> FixedSizeParser(byte command, int size)
|
||||||
|
{
|
||||||
|
return (BinaryReader current) =>
|
||||||
|
{
|
||||||
|
var lastByte = current.ReadBytes(size - 2).Last();
|
||||||
|
if (lastByte != 0xFF)
|
||||||
|
{
|
||||||
|
Debug.Instance.DebugMessage += $"Failed to parse command {command:X}, last byte is {lastByte:X}";
|
||||||
|
// throw new InvalidDataException($"Failed to parse command {command:X}, last byte is {lastByte:X}");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Action<BinaryReader> VariableSizeParser(byte command, int offset)
|
||||||
|
{
|
||||||
|
return (BinaryReader current) =>
|
||||||
|
{
|
||||||
|
var totalBytes = 2;
|
||||||
|
totalBytes += current.ReadBytes(offset - 2).Length;
|
||||||
|
for (var x = current.ReadByte(); x != 0xFF; x = current.ReadByte())
|
||||||
|
{
|
||||||
|
totalBytes += 1;
|
||||||
|
|
||||||
|
|
||||||
|
var size = ((x >> 4) + 1) * 4;
|
||||||
|
totalBytes += current.ReadBytes(size).Length;
|
||||||
|
}
|
||||||
|
totalBytes += 1;
|
||||||
|
var chk = totalBytes;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
var list = new List<(byte, Func<byte, int, Action<BinaryReader>>, int, string)>
|
||||||
|
{
|
||||||
|
(0x00, FixedSizeParser, 45, "展开建筑/建造碉堡(?)"),
|
||||||
|
(0x03, FixedSizeParser, 17, "开始升级"),
|
||||||
|
(0x04, FixedSizeParser, 17, "暂停/中止升级"),
|
||||||
|
(0x05, FixedSizeParser, 20, "开始生产单位或纳米核心"),
|
||||||
|
(0x06, FixedSizeParser, 20, "暂停/取消生产单位或纳米核心"),
|
||||||
|
(0x07, FixedSizeParser, 17, "开始建造建筑"),
|
||||||
|
(0x08, FixedSizeParser, 17, "暂停/取消建造建筑"),
|
||||||
|
(0x09, FixedSizeParser, 35, "摆放建筑"),
|
||||||
|
(0x0F, FixedSizeParser, 16, "(未知指令 0x0F)"),
|
||||||
|
(0x14, FixedSizeParser, 16, "移动"),
|
||||||
|
(0x15, FixedSizeParser, 16, "移动攻击(A)"),
|
||||||
|
(0x16, FixedSizeParser, 16, "强制移动/碾压(G)"),
|
||||||
|
(0x21, FixedSizeParser, 20, "[游戏每3秒自动产生的指令]"),
|
||||||
|
(0x2C, FixedSizeParser, 29, "队形移动(左右键)"),
|
||||||
|
(0x32, FixedSizeParser, 53, "释放技能或协议(多个目标,如侦察扫描协议)"),
|
||||||
|
(0x34, FixedSizeParser, 45, "[游戏自动产生的UUID]"),
|
||||||
|
(0x35, FixedSizeParser, 1049, "[玩家信息(?)]"),
|
||||||
|
(0x36, FixedSizeParser, 16, "倒车移动(D)"),
|
||||||
|
(0x5F, FixedSizeParser, 11, "(未知指令 0x5F)"),
|
||||||
|
|
||||||
|
(0x0A, VariableSizeParser, 2, "出售建筑"),
|
||||||
|
(0x0D, VariableSizeParser, 2, "右键攻击"),
|
||||||
|
(0x0E, VariableSizeParser, 2, "强制攻击(Ctrl)"),
|
||||||
|
(0x12, VariableSizeParser, 2, "(未知指令 0x12)"),
|
||||||
|
(0x1A, VariableSizeParser, 2, "停止(S)"),
|
||||||
|
(0x1B, VariableSizeParser, 2, "(未知指令 0x1B)"),
|
||||||
|
(0x28, VariableSizeParser, 2, "开始维修建筑"),
|
||||||
|
(0x29, VariableSizeParser, 2, "停止维修建筑"),
|
||||||
|
(0x2A, VariableSizeParser, 2, "选择所有单位(Q)"),
|
||||||
|
(0x2E, VariableSizeParser, 2, "切换警戒/侵略/固守/停火模式"),
|
||||||
|
(0x2F, VariableSizeParser, 2, "路径点模式(Alt)(?)"),
|
||||||
|
(0x37, VariableSizeParser, 2, "[游戏不定期自动产生的指令]"),
|
||||||
|
(0x47, VariableSizeParser, 2, "[游戏在第五帧自动产生的指令]"),
|
||||||
|
(0x48, VariableSizeParser, 2, "(未知指令 0x48)"),
|
||||||
|
(0x4C, VariableSizeParser, 2, "删除信标(或 F9?)"),
|
||||||
|
(0x4E, VariableSizeParser, 2, "选择协议"),
|
||||||
|
(0x52, VariableSizeParser, 2, "(未知指令 0x52)"),
|
||||||
|
(0xF5, VariableSizeParser, 5, "选择单位"),
|
||||||
|
(0xF6, VariableSizeParser, 5, "[未知指令,貌似会在展开兵营核心时自动产生?]"),
|
||||||
|
(0xF8, VariableSizeParser, 4, "鼠标左键单击/取消选择"),
|
||||||
|
(0xF9, VariableSizeParser, 2, "[可能是步兵自行从进驻的建筑撤出]"),
|
||||||
|
(0xFA, VariableSizeParser, 7, "创建编队"),
|
||||||
|
(0xFB, VariableSizeParser, 2, "选择编队"),
|
||||||
|
(0xFC, VariableSizeParser, 2, "(未知指令 0xFC)"),
|
||||||
|
(0xFD, VariableSizeParser, 7, "(未知指令 0xFD)"),
|
||||||
|
(0xFE, VariableSizeParser, 15, "释放技能或协议(无目标)"),
|
||||||
|
(0xFF, VariableSizeParser, 34, "释放技能或协议(单个目标)"),
|
||||||
|
};
|
||||||
|
|
||||||
|
var specialList = new List<(byte, Action<BinaryReader>, string)>
|
||||||
|
{
|
||||||
|
(0x01, ParseSpecialChunk0x01, "[游戏自动生成的指令]"),
|
||||||
|
(0x02, ParseSetRallyPoint0x02, "设计集结点"),
|
||||||
|
(0x0C, ParseUngarrison0x0C, "从进驻的建筑撤出(?)"),
|
||||||
|
(0x10, ParseGarrison0x10, "进驻建筑"),
|
||||||
|
(0x33, ParseUuid0x33, "[游戏自动生成的UUID指令]"),
|
||||||
|
(0x4B, ParsePlaceBeacon0x4B, "信标")
|
||||||
|
};
|
||||||
|
|
||||||
|
_commandParser = new Dictionary<byte, Action<BinaryReader>>();
|
||||||
|
_commandNames = new Dictionary<byte, string>();
|
||||||
|
|
||||||
|
foreach (var (id, maker, size, description) in list)
|
||||||
|
{
|
||||||
|
_commandParser.Add(id, maker(id, size));
|
||||||
|
_commandNames.Add(id, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (id, parser, description) in specialList)
|
||||||
|
{
|
||||||
|
_commandParser.Add(id, parser);
|
||||||
|
_commandNames.Add(id, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
_commandNames.Add(0x4D, "在信标里输入文字");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ReadCommandData(this BinaryReader reader, byte commandId)
|
||||||
|
{
|
||||||
|
if (_commandParser.TryGetValue(commandId, out var parser))
|
||||||
|
{
|
||||||
|
_commandParser[commandId](reader);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UnknownCommandParser(reader, commandId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsUnknownCommand(byte commandId)
|
||||||
|
{
|
||||||
|
return !_commandNames.ContainsKey(commandId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetCommandName(byte commandId)
|
||||||
|
{
|
||||||
|
return _commandNames.TryGetValue(commandId, out var storedName)
|
||||||
|
? storedName
|
||||||
|
: $"(未知指令 0x{commandId:X2})";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void UnknownCommandParser(BinaryReader current, byte commandId)
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var value = current.ReadByte();
|
||||||
|
if (value == 0xFF)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//return $"(未知指令 0x{commandID:2X})";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ParseSpecialChunk0x01(BinaryReader current)
|
||||||
|
{
|
||||||
|
var firstByte = current.ReadByte();
|
||||||
|
if (firstByte == 0xFF)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sixthByte = current.ReadBytes(5).Last();
|
||||||
|
if (sixthByte == 0xFF)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sixteenthByte = current.ReadBytes(10).Last();
|
||||||
|
var size = (int)(sixteenthByte + 1) * 4 + 14;
|
||||||
|
var lastByte = current.ReadBytes(size).Last();
|
||||||
|
if (lastByte != 0xFF)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ParseSetRallyPoint0x02(BinaryReader current)
|
||||||
|
{
|
||||||
|
var size = (current.ReadBytes(23).Last() + 1) * 2 + 1;
|
||||||
|
var lastByte = current.ReadBytes(size).Last();
|
||||||
|
if (lastByte != 0xFF)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ParseUngarrison0x0C(BinaryReader current)
|
||||||
|
{
|
||||||
|
current.ReadByte();
|
||||||
|
var size = (current.ReadByte() + 1) * 4 + 1;
|
||||||
|
var lastByte = current.ReadBytes(size).Last();
|
||||||
|
if (lastByte != 0xFF)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ParseGarrison0x10(BinaryReader current)
|
||||||
|
{
|
||||||
|
var type = current.ReadByte();
|
||||||
|
var size = -1;
|
||||||
|
if (type == 0x14)
|
||||||
|
{
|
||||||
|
size = 9;
|
||||||
|
}
|
||||||
|
else if (type == 0x04)
|
||||||
|
{
|
||||||
|
size = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastByte = current.ReadBytes(size).Last();
|
||||||
|
if (lastByte != 0xFF)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ParseUuid0x33(BinaryReader current)
|
||||||
|
{
|
||||||
|
current.ReadByte();
|
||||||
|
var firstStringLength = (int)current.ReadByte();
|
||||||
|
current.ReadBytes(firstStringLength + 1);
|
||||||
|
var secondStringLength = current.ReadByte() * 2;
|
||||||
|
var lastByte = current.ReadBytes(secondStringLength + 6).Last();
|
||||||
|
if (lastByte != 0xFF)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ParsePlaceBeacon0x4B(BinaryReader current)
|
||||||
|
{
|
||||||
|
var type = current.ReadByte();
|
||||||
|
var size = -1;
|
||||||
|
if (type == 0x04)
|
||||||
|
{
|
||||||
|
size = 5;
|
||||||
|
}
|
||||||
|
else if (type == 0x07)
|
||||||
|
{
|
||||||
|
size = 13;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastByte = current.ReadBytes(size).Last();
|
||||||
|
if (lastByte != 0xFF)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
315
ReplayFile/Replay.cs
Normal file
315
ReplayFile/Replay.cs
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
using AnotherReplayReader.Utils;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.ReplayFile
|
||||||
|
{
|
||||||
|
internal enum ReplayType
|
||||||
|
{
|
||||||
|
Skirmish,
|
||||||
|
Lan,
|
||||||
|
Online
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class Replay
|
||||||
|
{
|
||||||
|
public static readonly byte[] HeaderMagic = Encoding.ASCII.GetBytes("RA3 REPLAY HEADER");
|
||||||
|
public static readonly Dictionary<ReplayType, string> TypeStrings = new()
|
||||||
|
{
|
||||||
|
{ ReplayType.Skirmish, "遭遇战录像" },
|
||||||
|
{ ReplayType.Lan, "局域网录像" },
|
||||||
|
{ ReplayType.Online, "官网录像" },
|
||||||
|
};
|
||||||
|
public const double FrameRate = 15.0;
|
||||||
|
public const string PostCommentator = "post Commentator";
|
||||||
|
|
||||||
|
private readonly byte _replaySaverIndex;
|
||||||
|
private readonly byte[]? _rawHeader;
|
||||||
|
|
||||||
|
public string Path { get; }
|
||||||
|
public DateTime Date { get; }
|
||||||
|
public string MapName { get; }
|
||||||
|
public string MapPath { get; }
|
||||||
|
public ImmutableArray<Player> Players { get; }
|
||||||
|
public int NumberOfPlayingPlayers { get; }
|
||||||
|
public Mod Mod { get; }
|
||||||
|
public ReplayType Type { get; }
|
||||||
|
public bool HasCommentator { get; }
|
||||||
|
|
||||||
|
public long Size { get; }
|
||||||
|
public ReplayFooter? Footer { get; }
|
||||||
|
public ImmutableArray<ReplayChunk>? Body { get; }
|
||||||
|
|
||||||
|
public string FileName => System.IO.Path.GetFileNameWithoutExtension(Path);
|
||||||
|
public Player ReplaySaver => Players[_replaySaverIndex];
|
||||||
|
public string TypeString => TypeStrings[Type];
|
||||||
|
public bool HasFooter => Footer != null;
|
||||||
|
public ShortTimeSpan? Length => Footer?.ReplayLength;
|
||||||
|
|
||||||
|
public Replay(string path, bool parseBody = false) :
|
||||||
|
this(path, new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), parseBody)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Replay(string path, Stream stream, bool parseBody = false)
|
||||||
|
{
|
||||||
|
Path = path;
|
||||||
|
|
||||||
|
using var reader = new BinaryReader(stream);
|
||||||
|
Size = reader.BaseStream.Length;
|
||||||
|
|
||||||
|
var headerMagic = reader.ReadBytes(HeaderMagic.Length);
|
||||||
|
if (!headerMagic.SequenceEqual(HeaderMagic))
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"{Path} is not a replay, header is {BitConverter.ToString(headerMagic)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var isSkirmish = reader.ReadByte() == 0x04;
|
||||||
|
reader.ReadBytes(4 * 4); // version and builds
|
||||||
|
reader.ReadBytes(2); // commentary flag, and padding zero byte
|
||||||
|
|
||||||
|
reader.ReadUTF16String(); // title
|
||||||
|
reader.ReadUTF16String(); // description
|
||||||
|
MapName = reader.ReadUTF16String(); // map name
|
||||||
|
reader.ReadUTF16String(); // map id
|
||||||
|
|
||||||
|
NumberOfPlayingPlayers = reader.ReadByte();
|
||||||
|
|
||||||
|
for (var i = 0; i <= NumberOfPlayingPlayers; ++i)
|
||||||
|
{
|
||||||
|
reader.ReadUInt32();
|
||||||
|
reader.ReadUTF16String(); // utf16 player name
|
||||||
|
if (!isSkirmish)
|
||||||
|
{
|
||||||
|
reader.ReadByte(); // team
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset = reader.ReadInt32();
|
||||||
|
var cnc3MagicLength = reader.ReadInt32();
|
||||||
|
var headerSize = checked((int)(reader.BaseStream.Position + offset));
|
||||||
|
reader.ReadBytes(cnc3MagicLength);
|
||||||
|
|
||||||
|
var modInfo = reader.ReadBytes(22);
|
||||||
|
Mod = new Mod(Encoding.UTF8.GetString(modInfo));
|
||||||
|
|
||||||
|
var timeStamp = reader.ReadUInt32();
|
||||||
|
Date = DateTimeOffset.FromUnixTimeSeconds(timeStamp).DateTime;
|
||||||
|
|
||||||
|
reader.ReadBytes(31);
|
||||||
|
var descriptionsLength = reader.ReadInt32();
|
||||||
|
var description = Encoding.UTF8.GetString(reader.ReadBytes(descriptionsLength));
|
||||||
|
|
||||||
|
var entries = null as Dictionary<string, string>;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var query = from splitted in description.Split(';')
|
||||||
|
where !string.IsNullOrWhiteSpace(splitted)
|
||||||
|
select splitted.Split(new[] { '=' }, 2);
|
||||||
|
entries = query.ToDictionary(x => x[0], x => x[1]);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"Failed to parse string header of replay {Path}: \r\n{e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Players = entries["S"].Split(':')
|
||||||
|
.TakeWhile(x => !string.IsNullOrWhiteSpace(x) && x[0] != 'X')
|
||||||
|
.Select(x => new Player(x.Split(',')))
|
||||||
|
.ToImmutableArray();
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"Failed to parse playerdata from string header of replay {Path}: \r\n{exception}");
|
||||||
|
}
|
||||||
|
|
||||||
|
MapPath = entries["M"].Substring(3);
|
||||||
|
|
||||||
|
HasCommentator = !entries["PC"].Equals("-1");
|
||||||
|
|
||||||
|
var lanFlag = int.Parse(entries["GT"]) == 0;
|
||||||
|
if (lanFlag)
|
||||||
|
{
|
||||||
|
if (Players.First().PlayerIp == 0)
|
||||||
|
{
|
||||||
|
Type = ReplayType.Skirmish;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Type = ReplayType.Lan;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Type = ReplayType.Online;
|
||||||
|
}
|
||||||
|
|
||||||
|
_replaySaverIndex = reader.ReadByte();
|
||||||
|
|
||||||
|
reader.ReadBytes(8); // 8 bit paddings
|
||||||
|
var fileNameLength = reader.ReadInt32();
|
||||||
|
reader.ReadBytes(fileNameLength * 2);
|
||||||
|
reader.ReadBytes(16);
|
||||||
|
var verMagicLength = reader.ReadInt32();
|
||||||
|
reader.ReadBytes(verMagicLength);
|
||||||
|
reader.ReadBytes(85);
|
||||||
|
|
||||||
|
if (reader.BaseStream.Position != headerSize)
|
||||||
|
{
|
||||||
|
Debug.Instance.DebugMessage += $"Warning: the stored header size {headerSize} isn't correct (acutally {reader.BaseStream.Position})\r\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parseBody)
|
||||||
|
{
|
||||||
|
// jump to footer directly
|
||||||
|
reader.BaseStream.Seek(-4, SeekOrigin.End);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Footer = new ReplayFooter(reader, ReplayFooterOption.SeekToFooter);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Debug.Instance.DebugMessage += $"Failed to parse replay footer, replay might be corrupt: {e}\r\n";
|
||||||
|
Footer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var body = new List<ReplayChunk>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var timeCode = reader.ReadUInt32();
|
||||||
|
if (timeCode == ReplayFooter.Terminator)
|
||||||
|
{
|
||||||
|
Footer = new ReplayFooter(reader, ReplayFooterOption.CurrentlyAtFooter);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.Add(new ReplayChunk(timeCode, reader));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Debug.Instance.DebugMessage += $"Failed to parse replay body, replay might be corrupt: {e}\r\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[]? rawHeader = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 重新读取原来的整个录像头
|
||||||
|
reader.BaseStream.Seek(0, SeekOrigin.Begin);
|
||||||
|
rawHeader = reader.ReadBytes(headerSize);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Debug.Instance.DebugMessage += $"Warning: failed to read raw header: {e}\r\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (rawHeader.Length != headerSize)
|
||||||
|
{
|
||||||
|
Debug.Instance.DebugMessage += $"Warning: the stored header size {headerSize} isn't correct (raw header length = {rawHeader.Length})\r\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_rawHeader = rawHeader;
|
||||||
|
Body = body.ToImmutableArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Replay CloneHeader()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
using (var writer = new BinaryWriter(stream, Encoding.UTF8, true))
|
||||||
|
{
|
||||||
|
WriteTo(writer);
|
||||||
|
}
|
||||||
|
stream.Position = 0;
|
||||||
|
return new Replay(Path, stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool PathEquals(Replay replay) => PathEquals(replay.Path);
|
||||||
|
public bool PathEquals(string path) => Path.Equals(path, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public string GetDetails()
|
||||||
|
{
|
||||||
|
static string GetSizeString(double size)
|
||||||
|
{
|
||||||
|
if (size > 1024 * 1024)
|
||||||
|
{
|
||||||
|
return $"{Math.Round(size / (1024 * 1024), 2)}MB";
|
||||||
|
}
|
||||||
|
return $"{Math.Round(size / 1024)}KB";
|
||||||
|
}
|
||||||
|
|
||||||
|
string size = GetSizeString(Size);
|
||||||
|
string length = Length?.ToString() ?? "录像已损坏,请先修复录像";
|
||||||
|
|
||||||
|
var replaySaver = _replaySaverIndex < Players.Length
|
||||||
|
? ReplaySaver.PlayerName
|
||||||
|
: "[无法获取保存录像的玩家]";
|
||||||
|
|
||||||
|
using var writer = new StringWriter();
|
||||||
|
writer.WriteLine("文件名:{0}", FileName);
|
||||||
|
writer.WriteLine("大小:{0}", size);
|
||||||
|
writer.WriteLine("地图:{0}", MapName);
|
||||||
|
writer.WriteLine("日期:{0}", Date);
|
||||||
|
writer.WriteLine("长度:{0}", length);
|
||||||
|
writer.WriteLine("录像类别:{0}", TypeString);
|
||||||
|
writer.WriteLine("这个文件是{0}保存的", replaySaver);
|
||||||
|
writer.WriteLine("玩家列表:");
|
||||||
|
foreach (var player in Players)
|
||||||
|
{
|
||||||
|
if (player == Players.Last() && player.PlayerName.Equals(PostCommentator))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var factionName = ModData.GetFaction(Mod, player.FactionId).Name;
|
||||||
|
writer.WriteLine($"{player.PlayerName},{factionName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return writer.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteTo(BinaryWriter writer) => WriteTo(writer, false);
|
||||||
|
|
||||||
|
private void WriteTo(BinaryWriter writer, bool skipBody)
|
||||||
|
{
|
||||||
|
if ((_rawHeader is null || Body is null) && !skipBody)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Replay body must be parsed before writing replay");
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Write(_rawHeader);
|
||||||
|
|
||||||
|
var lastTimeCode = Footer?.FinalTimeCode;
|
||||||
|
if (Body is not null)
|
||||||
|
{
|
||||||
|
foreach (var chunk in Body)
|
||||||
|
{
|
||||||
|
lastTimeCode = chunk.TimeCode;
|
||||||
|
writer.Write(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Footer is not null)
|
||||||
|
{
|
||||||
|
writer.Write(Footer);
|
||||||
|
}
|
||||||
|
else if (lastTimeCode is uint lastTimeCodeValue)
|
||||||
|
{
|
||||||
|
writer.Write(new ReplayFooter(lastTimeCodeValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
40
ReplayFile/ReplayChunk.cs
Normal file
40
ReplayFile/ReplayChunk.cs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.ReplayFile
|
||||||
|
{
|
||||||
|
internal sealed class ReplayChunk
|
||||||
|
{
|
||||||
|
private readonly byte[] _data;
|
||||||
|
|
||||||
|
public uint TimeCode { get; }
|
||||||
|
public byte Type { get; }
|
||||||
|
|
||||||
|
public TimeSpan Time => TimeSpan.FromSeconds(TimeCode / Replay.FrameRate);
|
||||||
|
|
||||||
|
public ReplayChunk(uint timeCode, BinaryReader reader)
|
||||||
|
{
|
||||||
|
TimeCode = timeCode; // reader.ReadUInt32();
|
||||||
|
Type = reader.ReadByte();
|
||||||
|
var chunkSize = reader.ReadInt32();
|
||||||
|
_data = reader.ReadBytes(chunkSize);
|
||||||
|
if (reader.ReadInt32() != 0)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException("Replay Chunk not ended with zero");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public BinaryReader GetReader() => new(new MemoryStream(_data, false));
|
||||||
|
|
||||||
|
public void WriteTo(BinaryWriter writer)
|
||||||
|
{
|
||||||
|
writer.Write(TimeCode);
|
||||||
|
writer.Write(Type);
|
||||||
|
writer.Write(_data.Length);
|
||||||
|
writer.Write(_data);
|
||||||
|
writer.Write(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
40
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
92
ReplayFile/ReplayFooter.cs
Normal file
92
ReplayFile/ReplayFooter.cs
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.ReplayFile
|
||||||
|
{
|
||||||
|
internal enum ReplayFooterOption
|
||||||
|
{
|
||||||
|
SeekToFooter,
|
||||||
|
CurrentlyAtFooter,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class ReplayFooter
|
||||||
|
{
|
||||||
|
public const uint Terminator = 0x7FFFFFFF;
|
||||||
|
public static readonly byte[] FooterString = Encoding.ASCII.GetBytes("RA3 REPLAY FOOTER");
|
||||||
|
|
||||||
|
private readonly byte[] _data;
|
||||||
|
|
||||||
|
public uint FinalTimeCode { get; }
|
||||||
|
public TimeSpan ReplayLength => TimeSpan.FromSeconds(FinalTimeCode / Replay.FrameRate);
|
||||||
|
|
||||||
|
public ReplayFooter(BinaryReader reader, ReplayFooterOption option)
|
||||||
|
{
|
||||||
|
var currentPosition = reader.BaseStream.Position;
|
||||||
|
reader.BaseStream.Seek(-4, SeekOrigin.End);
|
||||||
|
var footerLength = reader.ReadInt32();
|
||||||
|
|
||||||
|
if (option == ReplayFooterOption.SeekToFooter)
|
||||||
|
{
|
||||||
|
currentPosition = reader.BaseStream.Length - footerLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.BaseStream.Seek(currentPosition, SeekOrigin.Begin);
|
||||||
|
var footer = reader.ReadBytes(footerLength);
|
||||||
|
|
||||||
|
if (footer.Length != footerLength || reader.BaseStream.Position != reader.BaseStream.Length)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException("Invalid footer");
|
||||||
|
}
|
||||||
|
|
||||||
|
using var footerStream = new MemoryStream(footer);
|
||||||
|
using var footerReader = new BinaryReader(footerStream);
|
||||||
|
var footerString = footerReader.ReadBytes(17);
|
||||||
|
if (!footerString.SequenceEqual(FooterString))
|
||||||
|
{
|
||||||
|
throw new InvalidDataException("Invalid footer, no footer string");
|
||||||
|
}
|
||||||
|
FinalTimeCode = footerReader.ReadUInt32();
|
||||||
|
_data = footerReader.ReadBytes(footer.Length - 25);
|
||||||
|
if (footerReader.ReadInt32() != footerLength)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReplayFooter(uint finalTimeCode)
|
||||||
|
{
|
||||||
|
FinalTimeCode = finalTimeCode;
|
||||||
|
_data = new byte[] { 0x02, 0x1A, 0x00, 0x00, 0x00 };
|
||||||
|
}
|
||||||
|
|
||||||
|
public float[]? TryGetKillDeathRatios()
|
||||||
|
{
|
||||||
|
if (_data.Length < 24)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ratios = new float[6];
|
||||||
|
using var stream = new MemoryStream(_data, _data.Length - 24, 24);
|
||||||
|
using var reader = new BinaryReader(stream);
|
||||||
|
for (var i = 0; i < ratios.Length; ++i)
|
||||||
|
{
|
||||||
|
ratios[i] = reader.ReadSingle();
|
||||||
|
}
|
||||||
|
return ratios;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteTo(BinaryWriter writer)
|
||||||
|
{
|
||||||
|
writer.Write(Terminator);
|
||||||
|
writer.Write(FooterString);
|
||||||
|
writer.Write(FinalTimeCode);
|
||||||
|
writer.Write(_data);
|
||||||
|
writer.Write(FooterString.Length + _data.Length + 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
BIN
TechnologyAssembler.Core.dll
Normal file
BIN
TechnologyAssembler.Core.dll
Normal file
Binary file not shown.
90
UpdateChecker.cs
Normal file
90
UpdateChecker.cs
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
using AnotherReplayReader.Utils;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader
|
||||||
|
{
|
||||||
|
internal class UpdateCheckerVersionData
|
||||||
|
{
|
||||||
|
public string NewVersion { get; }
|
||||||
|
public string Description { get; }
|
||||||
|
public ImmutableArray<string> Urls { get; }
|
||||||
|
|
||||||
|
public UpdateCheckerVersionData(string newVersion,
|
||||||
|
string description,
|
||||||
|
ImmutableArray<string> urls)
|
||||||
|
{
|
||||||
|
NewVersion = newVersion;
|
||||||
|
Description = description;
|
||||||
|
Urls = urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsNewVersion() => NewVersion != App.Version;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class UpdateChecker
|
||||||
|
{
|
||||||
|
public const string CheckForUpdatesKey = "checkForUpdates";
|
||||||
|
public const string CachedDataKey = "cachedUpdateData";
|
||||||
|
private static readonly ImmutableArray<string> _updateSources = new[]
|
||||||
|
{
|
||||||
|
"https://lanyi.altervista.org/playertable/file.json"
|
||||||
|
}.ToImmutableArray();
|
||||||
|
|
||||||
|
public static Task<UpdateCheckerVersionData> CheckForUpdates(Cache cache)
|
||||||
|
{
|
||||||
|
var taskSource = new TaskCompletionSource<UpdateCheckerVersionData>();
|
||||||
|
if (cache.GetOrDefault(CheckForUpdatesKey, false) is not true)
|
||||||
|
{
|
||||||
|
return taskSource.Task;
|
||||||
|
}
|
||||||
|
foreach (var source in _updateSources)
|
||||||
|
{
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = await DownloadAndVerify(source).ConfigureAwait(false);
|
||||||
|
if (data.IsNewVersion())
|
||||||
|
{
|
||||||
|
cache.Set(CachedDataKey, data);
|
||||||
|
taskSource.TrySetResult(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Debug.Instance.DebugMessage += $"Update check failed: {e}";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return taskSource.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<UpdateCheckerVersionData> DownloadAndVerify(string url)
|
||||||
|
{
|
||||||
|
var payload = await Network.HttpGetJson<VerifierPayload>(url)
|
||||||
|
?? throw new InvalidDataException(nameof(VerifierPayload) + " is null");
|
||||||
|
if (!Verifier.Verify(payload))
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(nameof(VerifierPayload) + " cannot be verified");
|
||||||
|
}
|
||||||
|
return JsonSerializer.Deserialize<UpdateCheckerVersionData>(payload.ByteData.Value, Network.CommonJsonOptions)
|
||||||
|
?? throw new InvalidDataException(nameof(UpdateCheckerVersionData) + " is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Sign()
|
||||||
|
{
|
||||||
|
var sample = new UpdateCheckerVersionData(App.Version, "Alice Margatroid", _updateSources);
|
||||||
|
var sampleText = JsonSerializer.Serialize(sample, Network.CommonJsonOptions);
|
||||||
|
Verifier.Sign(sampleText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
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/Network.cs
Normal file
27
Utils/Network.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
using System.Net.Http;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Web;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.Utils
|
||||||
|
{
|
||||||
|
public static class Network
|
||||||
|
{
|
||||||
|
public static readonly JsonSerializerOptions CommonJsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string UrlEncode(string text) => HttpUtility.UrlEncode(text);
|
||||||
|
|
||||||
|
public static async Task<T?> HttpGetJson<T>(string url,
|
||||||
|
CancellationToken cancelToken = default)
|
||||||
|
{
|
||||||
|
using var client = new HttpClient();
|
||||||
|
using var response = await client.GetAsync(url, cancelToken).ConfigureAwait(false);
|
||||||
|
using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||||
|
return await JsonSerializer.DeserializeAsync<T>(stream, CommonJsonOptions, cancelToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
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(" ", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
43
Utils/RegistryUtils.cs
Normal file
43
Utils/RegistryUtils.cs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
using Microsoft.Win32;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.Utils
|
||||||
|
{
|
||||||
|
public static class RegistryUtils
|
||||||
|
{
|
||||||
|
public static string? Retrieve32(RegistryHive hive, string path, string value)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var view32 = RegistryKey.OpenBaseKey(hive, RegistryView.Registry32);
|
||||||
|
using var key = view32.OpenSubKey(path, false);
|
||||||
|
return key?.GetValue(value) as string;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Debug.Instance.DebugMessage += $"Failed to retrieve registy {hive}:{path}:{value}: {e}";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? RetrieveInHklm64(string path, string value)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var view64 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64);
|
||||||
|
using var key = view64?.OpenSubKey(path, false);
|
||||||
|
return key?.GetValue(value) as string;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Debug.Instance.DebugMessage += $"Failed to retrieve registy HKLM64:{path}:{value}: {e}";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? RetrieveInRa3(RegistryHive hive, string value)
|
||||||
|
{
|
||||||
|
return Retrieve32(hive, @"Software\Electronic Arts\Electronic Arts\Red Alert 3", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
Utils/ReplayPinyinList.cs
Normal file
56
Utils/ReplayPinyinList.cs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
using AnotherReplayReader.ReplayFile;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.Utils
|
||||||
|
{
|
||||||
|
internal class ReplayPinyinList
|
||||||
|
{
|
||||||
|
public ImmutableArray<Replay> Replays { get; } = ImmutableArray<Replay>.Empty;
|
||||||
|
public ImmutableArray<ReplayPinyinData> Pinyins { get; } = ImmutableArray<ReplayPinyinData>.Empty;
|
||||||
|
|
||||||
|
public ReplayPinyinList() :
|
||||||
|
this(ImmutableArray<Replay>.Empty)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReplayPinyinList(ImmutableArray<Replay> replay) :
|
||||||
|
this(replay,
|
||||||
|
replay.Select(replay => new ReplayPinyinData(replay)).ToImmutableArray())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReplayPinyinList(ImmutableArray<Replay> replay,
|
||||||
|
ImmutableArray<ReplayPinyinData> pinyins)
|
||||||
|
{
|
||||||
|
Replays = replay;
|
||||||
|
Pinyins = pinyins;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReplayPinyinList SetItem(int index, Replay replay)
|
||||||
|
{
|
||||||
|
return new(Replays.SetItem(index, replay),
|
||||||
|
Pinyins.SetItem(index, new(replay)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReplayPinyinData
|
||||||
|
{
|
||||||
|
public Replay Replay { get; }
|
||||||
|
public string? PinyinDetails { get; }
|
||||||
|
public string? PinyinMod { get; }
|
||||||
|
|
||||||
|
public ReplayPinyinData(Replay replay)
|
||||||
|
{
|
||||||
|
Replay = replay;
|
||||||
|
PinyinDetails = replay.GetDetails().ToPinyin();
|
||||||
|
PinyinMod = replay.Mod.ModName.ToPinyin();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MatchPinyin(string? pinyin)
|
||||||
|
{
|
||||||
|
return PinyinDetails?.ContainsIgnoreCase(pinyin) is true
|
||||||
|
|| PinyinMod?.ContainsIgnoreCase(pinyin) is true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
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;
|
||||||
|
}
|
||||||
|
}
|
31
Utils/TaskQueue.cs
Normal file
31
Utils/TaskQueue.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.Utils
|
||||||
|
{
|
||||||
|
internal class TaskQueue
|
||||||
|
{
|
||||||
|
private readonly object _lock = new();
|
||||||
|
private readonly Dispatcher _dispatcher;
|
||||||
|
private Task _current = Task.CompletedTask;
|
||||||
|
|
||||||
|
public TaskQueue(Dispatcher dispatcher)
|
||||||
|
{
|
||||||
|
_dispatcher = dispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Enqueue(Func<Task> getTask, CancellationToken cancelToken)
|
||||||
|
{
|
||||||
|
using var locker = new Lock(_lock);
|
||||||
|
_current = _current.ContinueWith(async t =>
|
||||||
|
{
|
||||||
|
var task = await _dispatcher.InvokeAsync(getTask, DispatcherPriority.Background, cancelToken);
|
||||||
|
await task.ConfigureAwait(false);
|
||||||
|
}, cancelToken).Unwrap();
|
||||||
|
return _current.IgnoreCancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
115
Utils/Verifier.cs
Normal file
115
Utils/Verifier.cs
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.Utils
|
||||||
|
{
|
||||||
|
internal class VerifierPayload
|
||||||
|
{
|
||||||
|
public string Data { get; }
|
||||||
|
public string Signature { get; }
|
||||||
|
[JsonIgnore]
|
||||||
|
public Lazy<byte[]> ByteData { get; }
|
||||||
|
[JsonIgnore]
|
||||||
|
public Lazy<byte[]> ByteSignature { get; }
|
||||||
|
|
||||||
|
public VerifierPayload(string data, string signature)
|
||||||
|
{
|
||||||
|
Data = data;
|
||||||
|
Signature = signature;
|
||||||
|
ByteData = new(() => Convert.FromBase64String(Data),
|
||||||
|
LazyThreadSafetyMode.PublicationOnly);
|
||||||
|
ByteSignature = new(() => Convert.FromBase64String(Signature),
|
||||||
|
LazyThreadSafetyMode.PublicationOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static VerifierPayload FromBytes(byte[] data, byte[] signature)
|
||||||
|
{
|
||||||
|
return new(Convert.ToBase64String(data), Convert.ToBase64String(signature));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class Verifier
|
||||||
|
{
|
||||||
|
private const string _publicKey = @"<RSAKeyValue><Modulus>3vw5CoFRDFt2ri4jLDTu75cw1U/tCRjya7q8X/IdULaOJOYG8C+uqrF2Atb4ou+4SrmF+bvJM9cFsf3yO7XpeIDpkxD3KGbIEw+0JixTIIm+y5xlLKDDwbZHnYjJOBTt6JBn0yqwx7vY2UEZIcRU6wlOmUapnkpiaC2anNhSPqk=</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>";
|
||||||
|
|
||||||
|
public static bool Verify(VerifierPayload payload)
|
||||||
|
{
|
||||||
|
using var rsa = new RSACng();
|
||||||
|
rsa.FromXmlString(_publicKey);
|
||||||
|
return rsa.VerifyData(payload.ByteData.Value, payload.ByteSignature.Value, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Sign(object sample)
|
||||||
|
{
|
||||||
|
var fileName = Path.GetTempFileName();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ea = Enumerable.Repeat<byte>(0xEA, 1024).ToArray();
|
||||||
|
const string splitter = "|DuplexBarrier|";
|
||||||
|
var isByteArray = false;
|
||||||
|
if (sample is string sampleText)
|
||||||
|
{
|
||||||
|
File.WriteAllText(fileName, $"输入私钥信息以及需要签名的数据,用 `{splitter}` 分开\r\n\r\n{sampleText}");
|
||||||
|
}
|
||||||
|
else if (sample is byte[] readyArray)
|
||||||
|
{
|
||||||
|
isByteArray = true;
|
||||||
|
File.WriteAllText(fileName, $"输入私钥信息以及需要签名的数据{splitter}{Convert.ToBase64String(readyArray)}{splitter}");
|
||||||
|
}
|
||||||
|
using (var process = Process.Start("notepad.exe", fileName))
|
||||||
|
{
|
||||||
|
process.WaitForExit();
|
||||||
|
}
|
||||||
|
var splitted = File.ReadAllText(fileName).Split(new[] { splitter }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
File.WriteAllBytes(fileName, ea);
|
||||||
|
byte[] bytes;
|
||||||
|
byte[] signature;
|
||||||
|
|
||||||
|
using (var rsa = new RSACng())
|
||||||
|
{
|
||||||
|
rsa.FromXmlString(splitted[0]);
|
||||||
|
var text = splitted[1];
|
||||||
|
splitted = null;
|
||||||
|
GC.Collect();
|
||||||
|
MessageBox.Show(text, $"快来确认一下~");
|
||||||
|
bytes = isByteArray
|
||||||
|
? Convert.FromBase64String(text)
|
||||||
|
: Encoding.UTF8.GetBytes(text);
|
||||||
|
signature = rsa.SignData(bytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
|
||||||
|
}
|
||||||
|
GC.Collect();
|
||||||
|
var payload = VerifierPayload.FromBytes(bytes, signature);
|
||||||
|
var serialized = JsonSerializer.Serialize(payload, Network.CommonJsonOptions);
|
||||||
|
var choice = MessageBox.Show(serialized, "嗯哼", MessageBoxButton.YesNo);
|
||||||
|
while (choice == MessageBoxResult.Yes)
|
||||||
|
{
|
||||||
|
File.WriteAllText(fileName, serialized);
|
||||||
|
using (var process = Process.Start("notepad.exe", fileName))
|
||||||
|
{
|
||||||
|
process.WaitForExit();
|
||||||
|
}
|
||||||
|
using var rsa = new RSACng();
|
||||||
|
rsa.FromXmlString(_publicKey);
|
||||||
|
var checkContent = File.ReadAllText(fileName);
|
||||||
|
var check = JsonSerializer.Deserialize<VerifierPayload>(checkContent, Network.CommonJsonOptions) ?? new("", "");
|
||||||
|
var checkData = Convert.FromBase64String(check.Data);
|
||||||
|
var checkSignature = Convert.FromBase64String(check.Signature);
|
||||||
|
var verified = rsa.VerifyData(checkData, checkSignature, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
|
||||||
|
choice = MessageBox.Show($"结果:{verified}", "嗯哼", MessageBoxButton.YesNo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
File.Delete(fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
Utils/WpfExtensions.cs
Normal file
29
Utils/WpfExtensions.cs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.Utils
|
||||||
|
{
|
||||||
|
internal static class WpfExtensions
|
||||||
|
{
|
||||||
|
public static IEnumerable<T> FindVisualChildren<T>(this DependencyObject depObj) where T : DependencyObject
|
||||||
|
{
|
||||||
|
foreach (var x in LogicalTreeHelper.GetChildren(depObj))
|
||||||
|
{
|
||||||
|
if (x is not DependencyObject child)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child is T tchild)
|
||||||
|
{
|
||||||
|
yield return tchild;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var childOfChild in FindVisualChildren<T>(child))
|
||||||
|
{
|
||||||
|
yield return childOfChild;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
Window1.xaml
14
Window1.xaml
@ -8,15 +8,19 @@
|
|||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="Window1" Height="450" Width="800">
|
Title="Window1" Height="450" Width="800">
|
||||||
<Grid>
|
<Grid>
|
||||||
<TextBox x:Name="_ipField" HorizontalAlignment="Left" Height="16" Margin="45,27,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="155" TextChanged="OnIPFieldChanged" />
|
<TextBox x:Name="_ipField" HorizontalAlignment="Left" Height="16" Margin="45,27,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="155" TextChanged="OnIpFieldChanged" />
|
||||||
<TextBlock x:Name="textBlock" HorizontalAlignment="Left" Margin="29,28,0,0" TextWrapping="Wrap" Text="IP" VerticalAlignment="Top"/>
|
<TextBlock x:Name="textBlock" HorizontalAlignment="Left" Margin="29,28,0,0" TextWrapping="Wrap" Text="IP" VerticalAlignment="Top"/>
|
||||||
<TextBox x:Name="_idField" Height="16" Margin="294,27,92,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" />
|
<TextBox x:Name="_idField" Height="16" Margin="294,27,92,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" TextChanged="OnIpFieldChanged" />
|
||||||
<TextBlock x:Name="textBlock1" HorizontalAlignment="Left" Margin="205,28,0,0" TextWrapping="Wrap" Text="玩家名称 / 说明" VerticalAlignment="Top"/>
|
<TextBlock x:Name="textBlock1" HorizontalAlignment="Left" Margin="205,28,0,0" TextWrapping="Wrap" Text="玩家名称 / 说明" VerticalAlignment="Top"/>
|
||||||
<Button x:Name="_setIPButton" Content="上传" Margin="705,26,12,0" VerticalAlignment="Top" Click="OnClick"/>
|
<Button x:Name="_setIPButton" Content="上传" Margin="705,26,12,0" VerticalAlignment="Top" Click="OnClick"/>
|
||||||
<DataGrid x:Name="_dataGrid" Margin="20,60,12,19">
|
<DataGrid x:Name="_dataGrid"
|
||||||
|
Margin="20,60,12,19"
|
||||||
|
MouseDoubleClick="OnDataGridMouseDoubleClick"
|
||||||
|
IsReadOnly="True"
|
||||||
|
AutoGenerateColumns="False">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="IP" Binding="{Binding Path=IPString}"/>
|
<DataGridTextColumn Header="IP" Binding="{Binding Path=IpString}"/>
|
||||||
<DataGridTextColumn Header="玩家名称 / 说明" Binding="{Binding Path=ID}"/>
|
<DataGridTextColumn Header="玩家名称 / 说明" Binding="{Binding Path=Id}"/>
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
133
Window1.xaml.cs
133
Window1.xaml.cs
@ -1,12 +1,9 @@
|
|||||||
using System;
|
using AnotherReplayReader.Utils;
|
||||||
using System.Collections.Generic;
|
using System;
|
||||||
using System.IO;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Web;
|
|
||||||
using System.Web.Script.Serialization;
|
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
namespace AnotherReplayReader
|
||||||
@ -16,113 +13,97 @@ namespace AnotherReplayReader
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal partial class Window1 : Window
|
internal partial class Window1 : Window
|
||||||
{
|
{
|
||||||
private PlayerIdentity _identity;
|
|
||||||
|
|
||||||
public Window1(PlayerIdentity identity)
|
public Window1()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_identity = identity;
|
Refresh(true);
|
||||||
Refresh();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void Refresh()
|
private async void Refresh(bool showCached)
|
||||||
{
|
{
|
||||||
Dispatcher.Invoke(() => _setIPButton.IsEnabled = false);
|
_setIPButton.IsEnabled = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Dispatcher.Invoke(() =>
|
var loading = new IpAndPlayer[] { new() { Ip = 0, Id = "正在加载..." } };
|
||||||
|
_dataGrid.ItemsSource = loading;
|
||||||
|
if (showCached)
|
||||||
{
|
{
|
||||||
_dataGrid.Items.Clear();
|
await Display();
|
||||||
_dataGrid.Items.Add(new IPAndPlayer { IP = 0, ID = "正在加载..." });
|
_dataGrid.ItemsSource = loading.Concat(_dataGrid.ItemsSource.Cast<IpAndPlayer>());
|
||||||
});
|
}
|
||||||
|
await Display();
|
||||||
await _identity.Fetch();
|
|
||||||
|
|
||||||
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 = "")
|
private async Task Display(string filter = "", string nameFilter = "")
|
||||||
{
|
{
|
||||||
var newList = _identity.AsSortedList().Where(x => x.IPString.StartsWith(filter));
|
|
||||||
Dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
_dataGrid.Items.Clear();
|
|
||||||
foreach (var item in newList)
|
|
||||||
{
|
|
||||||
_dataGrid.Items.Add(item);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnClick(object sender, RoutedEventArgs e)
|
private async void OnClick(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
Dispatcher.Invoke(() => _setIPButton.IsEnabled = false);
|
_setIPButton.IsEnabled = false;
|
||||||
|
try
|
||||||
await Task.Run(() =>
|
|
||||||
{
|
{
|
||||||
var ipText = Dispatcher.Invoke(() => _ipField.Text);
|
var ipText = _ipField.Text;
|
||||||
|
|
||||||
if (!IPAddress.TryParse(ipText, out var ip))
|
if (!IPAddress.TryParse(ipText, out var ip))
|
||||||
{
|
{
|
||||||
Dispatcher.Invoke(() => MessageBox.Show(this, "IP格式不正确"));
|
MessageBox.Show(this, "IP 格式不正确");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
var idText = _idField.Text;
|
||||||
var idText = Dispatcher.Invoke(() => _idField.Text);
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(idText))
|
if (string.IsNullOrWhiteSpace(idText))
|
||||||
{
|
{
|
||||||
var result = Dispatcher.Invoke(() => MessageBox.Show(this, "你没填输入任何说明,是否确认继续?", "注意", MessageBoxButton.OKCancel));
|
var choice = MessageBox.Show(this, "没有输入任何关于该玩家的说明,是否继续?", "注意", MessageBoxButton.OKCancel);
|
||||||
if(result != MessageBoxResult.OK)
|
if (choice != MessageBoxResult.OK)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
var result = await UpdateIpTable(ip, idText);
|
||||||
|
if (!result)
|
||||||
{
|
{
|
||||||
var bytes = ip.GetAddressBytes();
|
MessageBox.Show(this, "设置 IP 表失败");
|
||||||
var ipNum = (uint)bytes[0] * 256 * 256 * 256 + bytes[1] * 256 * 256 + bytes[2] * 256 + bytes[3];
|
|
||||||
var text = HttpUtility.UrlEncode(idText);
|
|
||||||
|
|
||||||
var key = HttpUtility.UrlEncode(Auth.GetKey());
|
|
||||||
var request = WebRequest.Create($"https://lanyi.altervista.org/playertable/playertable.php?do=setIP&ip={ipNum}&id={text}&key={key}");
|
|
||||||
|
|
||||||
using (var stream = request.GetResponse().GetResponseStream())
|
|
||||||
using (var reader = new StreamReader(stream))
|
|
||||||
{
|
|
||||||
var response = reader.ReadToEnd();
|
|
||||||
var serializer = new JavaScriptSerializer();
|
|
||||||
var result = serializer.Deserialize<bool>(response);
|
|
||||||
if(!result)
|
|
||||||
{
|
|
||||||
Dispatcher.Invoke(() => MessageBox.Show(this, "设置IP表失败"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
Dispatcher.Invoke(() => MessageBox.Show(this, $"设置IP表时发生错误。\r\n{exception}"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
Refresh(false);
|
||||||
|
}
|
||||||
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 async Task<bool> UpdateIpTable(IPAddress ip, string idText)
|
||||||
{
|
{
|
||||||
await Task.Run(() =>
|
var bytes = ip.GetAddressBytes();
|
||||||
{
|
var ipNum = (uint)bytes[0] * 256 * 256 * 256 + bytes[1] * 256 * 256 + bytes[2] * 256 + bytes[3];
|
||||||
var fieldText = Dispatcher.Invoke(() => _ipField.Text);
|
return false;
|
||||||
Display(fieldText);
|
}
|
||||||
});
|
|
||||||
|
private async void OnIpFieldChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
|
||||||
|
{
|
||||||
|
var ipText = _ipField.Text;
|
||||||
|
var idText = _idField.Text;
|
||||||
|
await Display(ipText, idText);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDataGridMouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
76
app.manifest
Normal file
76
app.manifest
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
|
||||||
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||||
|
<security>
|
||||||
|
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<!-- UAC Manifest Options
|
||||||
|
If you want to change the Windows User Account Control level replace the
|
||||||
|
requestedExecutionLevel node with one of the following.
|
||||||
|
|
||||||
|
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||||
|
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||||
|
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
|
||||||
|
|
||||||
|
Specifying requestedExecutionLevel element will disable file and registry virtualization.
|
||||||
|
Remove this element if your application requires this virtualization for backwards
|
||||||
|
compatibility.
|
||||||
|
-->
|
||||||
|
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||||
|
</requestedPrivileges>
|
||||||
|
</security>
|
||||||
|
</trustInfo>
|
||||||
|
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<!-- A list of the Windows versions that this application has been tested on
|
||||||
|
and is designed to work with. Uncomment the appropriate elements
|
||||||
|
and Windows will automatically select the most compatible environment. -->
|
||||||
|
|
||||||
|
<!-- Windows Vista -->
|
||||||
|
<!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
|
||||||
|
|
||||||
|
<!-- Windows 7 -->
|
||||||
|
<!--<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />-->
|
||||||
|
|
||||||
|
<!-- Windows 8 -->
|
||||||
|
<!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->
|
||||||
|
|
||||||
|
<!-- Windows 8.1 -->
|
||||||
|
<!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->
|
||||||
|
|
||||||
|
<!-- Windows 10 -->
|
||||||
|
<!--<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />-->
|
||||||
|
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
|
||||||
|
<!-- Indicates that the application is DPI-aware and will not be automatically scaled by Windows at higher
|
||||||
|
DPIs. Windows Presentation Foundation (WPF) applications are automatically DPI-aware and do not need
|
||||||
|
to opt in. Windows Forms applications targeting .NET Framework 4.6 that opt into this setting, should
|
||||||
|
also set the 'EnableWindowsFormsHighDpiAutoResizing' setting to 'true' in their app.config.
|
||||||
|
|
||||||
|
Makes the application long-path aware. See https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation -->
|
||||||
|
<!--
|
||||||
|
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<windowsSettings>
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
||||||
|
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
|
||||||
|
</windowsSettings>
|
||||||
|
</application>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- Enable themes for Windows common controls and dialogs (Windows XP and later) -->
|
||||||
|
<dependency>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity
|
||||||
|
type="win32"
|
||||||
|
name="Microsoft.Windows.Common-Controls"
|
||||||
|
version="6.0.0.0"
|
||||||
|
processorArchitecture="*"
|
||||||
|
publicKeyToken="6595b64144ccf1df"
|
||||||
|
language="*"
|
||||||
|
/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</dependency>
|
||||||
|
</assembly>
|
95
autosave.txt
95
autosave.txt
@ -1,95 +0,0 @@
|
|||||||
private async Task AutoSaveReplays()
|
|
||||||
{
|
|
||||||
const string ourPrefix = "自动保存";
|
|
||||||
|
|
||||||
// filename and last write time
|
|
||||||
Dictionary<string, DateTime> previousFiles = new Dictionary<string, DateTime>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
// filename and file size
|
|
||||||
Dictionary<string, long> lastReplays = new Dictionary<string, long>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
while(true)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var changed = from fileName in Directory.GetFiles(_properties.ReplayFolderPath, "*.RA3Replay")
|
|
||||||
let info = new FileInfo(fileName)
|
|
||||||
where !info.Name.StartsWith(ourPrefix)
|
|
||||||
where !previousFiles.ContainsKey(info.FullName) || previousFiles[info.FullName] != info.LastWriteTimeUtc
|
|
||||||
select info;
|
|
||||||
|
|
||||||
foreach (var info in changed)
|
|
||||||
{
|
|
||||||
previousFiles[info.FullName] = info.LastWriteTimeUtc;
|
|
||||||
}
|
|
||||||
|
|
||||||
var replays = changed.Select(info =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return new Replay(info.FullName);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}).Where(replay => replay != null);
|
|
||||||
|
|
||||||
var newLastReplays = from replay in replays
|
|
||||||
let threshold = Math.Abs((DateTime.UtcNow - replay.Date).TotalSeconds)
|
|
||||||
where threshold < 20
|
|
||||||
select replay;
|
|
||||||
|
|
||||||
var toBeChecked = newLastReplays.ToDictionary(replay => replay.FileName, StringComparer.OrdinalIgnoreCase);
|
|
||||||
foreach (var savedLastReplay in lastReplays)
|
|
||||||
{
|
|
||||||
if (!toBeChecked.ContainsKey(savedLastReplay.Key))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
toBeChecked.Add(savedLastReplay.Key, new Replay(savedLastReplay.Key));
|
|
||||||
}
|
|
||||||
catch(Exception)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var kv in toBeChecked)
|
|
||||||
{
|
|
||||||
var replay = kv.Value;
|
|
||||||
if (lastReplays.TryGetValue(kv.Key, out var fileSize))
|
|
||||||
{
|
|
||||||
if (fileSize == replay.Size)
|
|
||||||
{
|
|
||||||
// skip if size is not changed
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lastReplays[kv.Key] = replay.Size;
|
|
||||||
|
|
||||||
var date = replay.Date;
|
|
||||||
var numberOfPlayers = replay.NumberOfPlayingPlayers;
|
|
||||||
var playerString = $"{numberOfPlayers}名玩家";
|
|
||||||
if (replay.NumberOfPlayingPlayers <= 2)
|
|
||||||
{
|
|
||||||
var playingPlayers = from player in replay.Players
|
|
||||||
let faction = ModData.GetFaction(replay.Mod, player.FactionID)
|
|
||||||
where faction.Kind != FactionKind.Observer
|
|
||||||
select $"{player}({faction.Name})";
|
|
||||||
playerString = playingPlayers.Aggregate((x, y) => x + y);
|
|
||||||
}
|
|
||||||
|
|
||||||
var dateString = $"{date.Year}-{date.Month}-{date.Day}_{date.Hour}:{date.Minute}";
|
|
||||||
|
|
||||||
File.Copy(replay.FileName, $"{_properties.ReplayFolderPath}/{ourPrefix}-{playerString}{dateString}.RA3Replay");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(Exception e)
|
|
||||||
{
|
|
||||||
_ = Dispatcher.InvokeAsync(() => MessageBox.Show($"自动保存录像时出现错误:\r\n{e}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Delay(10 * 1000);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user