diff --git a/About.xaml b/About.xaml
index 38acf8e..823edef 100644
--- a/About.xaml
+++ b/About.xaml
@@ -14,20 +14,21 @@
-
-
-
-
+
+
+
+
-
+
-
-
-
+
+
+
+
-
+
-
+
diff --git a/About.xaml.cs b/About.xaml.cs
index b311812..4174e2a 100644
--- a/About.xaml.cs
+++ b/About.xaml.cs
@@ -1,17 +1,5 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+using System.Diagnostics;
using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Data;
-using System.Windows.Documents;
-using System.Windows.Input;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
-using System.Windows.Shapes;
namespace AnotherReplayReader
{
diff --git a/AnotherReplayReader.csproj b/AnotherReplayReader.csproj
index da12d7b..9fcf718 100644
--- a/AnotherReplayReader.csproj
+++ b/AnotherReplayReader.csproj
@@ -6,6 +6,7 @@
publish\
false
true
+ true
AnyCPU
@@ -20,19 +21,34 @@
-
-
-
-
-
-
+
-
+
+
+ TechnologyAssembler.Core.dll
+ true
+
+
+
+
-
-
-
+
+
+
+
+
+
+ True
+ True
+ Settings.settings
+
+
+
+
+ SettingsSingleFileGenerator
+ Settings.Designer.cs
+
\ No newline at end of file
diff --git a/APM.xaml b/ApmWindow.xaml
similarity index 96%
rename from APM.xaml
rename to ApmWindow.xaml
index 4738814..e892a4f 100644
--- a/APM.xaml
+++ b/ApmWindow.xaml
@@ -1,4 +1,4 @@
-
new DataValue(x)).ToArray();
}
- }
- internal static class DataTableFactory
- {
- static public List Get(Replay replay, PlayerIdentity identity)
+ public static List GetList(Replay replay, PlayerIdentity identity)
{
var list = new List
{
@@ -136,7 +134,7 @@ namespace AnotherReplayReader
};
if (replay.Type == ReplayType.Lan && identity.IsUsable)
{
- dataList.Add(new DataRow("局域网IP", replay.Players.Select(x => identity.QueryRealNameAndIP(x.PlayerIP))));
+ dataList.Add(new DataRow("局域网IP", replay.Players.Select(x => identity.QueryRealNameAndIP(x.PlayerIp))));
}
var commandCounts = replay.GetCommandCounts();
@@ -155,65 +153,54 @@ namespace AnotherReplayReader
return dataList;
}
}
+
///
/// APM.xaml 的交互逻辑
///
- internal partial class APM : Window
+ internal partial class ApmWindow : Window
{
- private PlayerIdentity _identity;
+ private readonly PlayerIdentity _identity;
- public APM(Replay replay, PlayerIdentity identity)
+ public ApmWindow(Replay replay, PlayerIdentity identity)
{
- InitializeComponent();
-
_identity = identity;
+ InitializeComponent();
+ InitializeApmWindowData(replay);
+ }
+ private async void InitializeApmWindowData(Replay replay)
+ {
if (_identity.IsUsable)
{
- Dispatcher.Invoke(() =>
- {
- _setPlayerButton.IsEnabled = true;
- _setPlayerButton.Visibility = Visibility.Visible;
- });
+ _setPlayerButton.IsEnabled = true;
+ _setPlayerButton.Visibility = Visibility.Visible;
}
else
{
- Dispatcher.Invoke(() =>
- {
- _setPlayerButton.IsEnabled = false;
- _setPlayerButton.Visibility = Visibility.Hidden;
- });
+ _setPlayerButton.IsEnabled = false;
+ _setPlayerButton.Visibility = Visibility.Hidden;
}
- Task.Run(() =>
+ try
{
- try
+ var dataList = await Task.Run(() => DataRow.GetList(replay, _identity));
+ _table.Items.Clear();
+ foreach (var row in dataList)
{
- var dataList = DataTableFactory.Get(replay, _identity);
- Dispatcher.Invoke(() =>
- {
- _table.Items.Clear();
- foreach (var row in dataList)
- {
- _table.Items.Add(row);
- }
+ _table.Items.Add(row);
+ }
- _table.Columns[1].Header = dataList[0].Player1Value;
- _table.Columns[2].Header = dataList[0].Player2Value;
- _table.Columns[3].Header = dataList[0].Player3Value;
- _table.Columns[4].Header = dataList[0].Player4Value;
- _table.Columns[5].Header = dataList[0].Player5Value;
- _table.Columns[6].Header = dataList[0].Player6Value;
- });
- }
- catch (Exception e)
- {
- Dispatcher.Invoke(() =>
- {
- MessageBox.Show($"加载录像信息失败:\r\n{e}");
- });
- }
- });
+ _table.Columns[1].Header = dataList[0].Player1Value;
+ _table.Columns[2].Header = dataList[0].Player2Value;
+ _table.Columns[3].Header = dataList[0].Player3Value;
+ _table.Columns[4].Header = dataList[0].Player4Value;
+ _table.Columns[5].Header = dataList[0].Player5Value;
+ _table.Columns[6].Header = dataList[0].Player6Value;
+ }
+ catch (Exception e)
+ {
+ MessageBox.Show($"加载录像信息失败:\r\n{e}");
+ }
}
private void OnSetPlayerButtonClick(object sender, RoutedEventArgs e)
@@ -222,7 +209,7 @@ namespace AnotherReplayReader
window1.ShowDialog();
}
- private void OnTableMouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
+ private void OnTableMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
_table.SelectAll();
}
diff --git a/App.xaml b/App.xaml
index 75c9b3c..23908b7 100644
--- a/App.xaml
+++ b/App.xaml
@@ -2,7 +2,8 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:AnotherReplayReader"
- StartupUri="MainWindow.xaml">
+ StartupUri="MainWindow.xaml"
+ ShutdownMode="OnMainWindowClose">
diff --git a/App.xaml.cs b/App.xaml.cs
index b45448e..efec509 100644
--- a/App.xaml.cs
+++ b/App.xaml.cs
@@ -1,8 +1,5 @@
using System;
-using System.Collections.Generic;
-using System.Configuration;
-using System.Data;
-using System.Linq;
+using System.Threading;
using System.Windows;
namespace AnotherReplayReader
@@ -12,5 +9,25 @@ namespace AnotherReplayReader
///
public partial class App : Application
{
+ private int _isInException = 0;
+ public App()
+ {
+ AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) =>
+ {
+ if (Interlocked.Increment(ref _isInException) > 1)
+ {
+ return;
+ }
+ Dispatcher.Invoke(() =>
+ {
+ const string message = "哎呀呀,出现了一些无法处理的问题,只能退出了。要不要尝试保存一下日志文件呢?";
+ var choice = MessageBox.Show($"{message}\r\n{eventArgs.ExceptionObject}", "自动录像机", MessageBoxButton.YesNo);
+ if (choice == MessageBoxResult.Yes)
+ {
+ Debug.Instance.RequestSave();
+ }
+ });
+ };
+ }
}
}
diff --git a/Auth.cs b/Auth.cs
index 4faf608..1098838 100644
--- a/Auth.cs
+++ b/Auth.cs
@@ -1,37 +1,33 @@
-using System;
+using Microsoft.Win32;
+using System;
using System.IO;
-using System.Collections.Generic;
using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
using System.Security.Cryptography;
-using Microsoft.Win32;
+using System.Text;
namespace AnotherReplayReader
{
internal static class Auth
{
- public static string ID { get; private set; }
+ public static string? ID { get; }
static Auth()
{
ID = null;
- var windowsID = null as string;
+ string? windowsID;
try
{
- using (var view64 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64))
- using (var winNt = view64?.OpenSubKey(@"Software\Microsoft\Windows NT\CurrentVersion", false))
- {
- windowsID = winNt?.GetValue("ProductId") as string;
- }
+ using var view64 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64);
+ using var winNt = view64?.OpenSubKey(@"Software\Microsoft\Windows NT\CurrentVersion", false);
+ windowsID = winNt?.GetValue("ProductId") as string;
}
- catch(Exception)
+ catch
{
return;
}
- var randomKey = null as string;
+ string? randomKey;
try
{
var folderPath = Cache.CacheDirectory;
@@ -41,34 +37,31 @@ namespace AnotherReplayReader
}
var keyPath = Path.Combine(folderPath, "id");
- if(!File.Exists(keyPath))
+ if (!File.Exists(keyPath))
{
File.WriteAllText(keyPath, Guid.NewGuid().ToString());
}
randomKey = File.ReadAllText(keyPath);
}
- catch(Exception)
- {
- return;
- }
-
- if(string.IsNullOrWhiteSpace(windowsID) || string.IsNullOrWhiteSpace(randomKey))
+ catch
{
return;
}
-
- using (var sha = SHA256.Create())
+ if (string.IsNullOrWhiteSpace(windowsID) || string.IsNullOrWhiteSpace(randomKey))
{
- var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(windowsID + randomKey));
- ID = string.Concat(hash.Skip(3).Take(10).Select(x => $"{x:X2}"));
+ return;
}
+
+ using var sha = SHA256.Create();
+ var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(windowsID + randomKey));
+ ID = string.Concat(hash.Skip(3).Take(10).Select(x => $"{x:X2}"));
}
public static string GetKey()
{
- if(ID == null)
+ if (ID == null)
{
return string.Empty;
}
@@ -82,7 +75,7 @@ namespace AnotherReplayReader
rng.GetNonZeroBytes(salt);
}
- for(var i = 0; i < bytes.Length; ++i)
+ for (var i = 0; i < bytes.Length; ++i)
{
bytes[i] = (byte)(bytes[i] ^ salt[i % salt.Length]);
}
diff --git a/BigMinimapCache.cs b/BigMinimapCache.cs
index 294c2e2..b090dd5 100644
--- a/BigMinimapCache.cs
+++ b/BigMinimapCache.cs
@@ -1,172 +1,35 @@
-using System;
-using System.Collections.Generic;
+using AnotherReplayReader.Utils;
+using Microsoft.Win32;
+using System;
using System.IO;
using System.Linq;
-using System.Text;
+using System.Text.RegularExpressions;
using System.Threading.Tasks;
-using Microsoft.Win32;
-using OpenSage.FileFormats.Big;
+using TechnologyAssembler.Core.IO;
namespace AnotherReplayReader
{
internal sealed class BigMinimapCache
{
- private sealed class CacheAdapter
- {
- public List Bigs { get; set; }
- public Dictionary MapsToBigs { get; set; }
+ private readonly object _lock = new();
+ private SkuDefFileSystemProvider? _skudefFileSystem = null;
- public CacheAdapter()
- {
- Bigs = new List();
- MapsToBigs = new Dictionary();
- }
+ public BigMinimapCache(string? ra3Directory)
+ {
+ Task.Run(() => Initialize(ra3Directory));
}
- //private Cache _cache;
- private volatile IReadOnlyDictionary _mapsToBigs = null;
-
- public BigMinimapCache(Cache cache, string ra3Directory)
+ public bool TryGetEntry(string path, out Stream? bigEntry)
{
- //_cache = cache;
+ bigEntry = null;
- Task.Run(() =>
- {
- try
- {
- if (!Directory.Exists(ra3Directory))
- {
- Debug.Instance.DebugMessage += $"Will not initialize BigMinimapCache because RA3Directory {ra3Directory} does not exist.\r\n";
- return;
- }
-
- var bigSet = ParseSkudefs(Directory.EnumerateFiles(ra3Directory, "*.SkuDef"));
-
- //var cached = _cache.GetOrDefault("bigsCache", new CacheAdapter());
- var mapsToBigs = new Dictionary();
-
- 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 ParseSkudefs(IEnumerable skudefs)
- {
- var skudefSet = new HashSet(skudefs.Select(x => x.ToLowerInvariant()));
- var unreadSkudefs = new HashSet();
- var bigSet = new HashSet();
-
- void ReadSkudefLine(string baseDirectory, string line, string expectedCommand, Action 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 onBaseDirectoryAndLine)
- {
- try
- {
- var baseDirectory = Path.GetDirectoryName(fileName).ToLowerInvariant();
- foreach (var line in File.ReadAllLines(fileName))
- {
- onBaseDirectoryAndLine(baseDirectory, line);
- }
- }
- catch (Exception exception)
- {
- Debug.Instance.DebugMessage += $"Exception when parsing skudef file:\r\n {exception}\r\n";
- }
- }
-
- foreach (var skudef in skudefSet)
- {
- ReadSkudef(skudef, (baseDirectory, line) =>
- {
- ReadSkudefLine(baseDirectory, line, "add-config", x =>
- {
- if (!skudefSet.Contains(x))
- {
- unreadSkudefs.Add(x);
- }
- });
-
- ReadSkudefLine(baseDirectory, line, "add-big", x => bigSet.Add(x));
- });
- }
-
- foreach (var skudef in unreadSkudefs)
- {
- ReadSkudef(skudef, (baseDirectory, line) =>
- {
- ReadSkudefLine(baseDirectory, line, "add-big", x => bigSet.Add(x));
- });
- }
-
- return bigSet;
- }
-
- public bool TryGetBigByEntryPath(string path, out BigArchive big)
- {
- big = null;
-
- if (_mapsToBigs == null)
+ using var locker = new Lock(_lock);
+ if (_skudefFileSystem is not { } fs)
{
return false;
}
- if (!_mapsToBigs.ContainsKey(path))
+ if (!fs.FileExists(path))
{
Debug.Instance.DebugMessage += $"Cannot find big entry [{path}].\r\n";
return false;
@@ -174,58 +37,75 @@ namespace AnotherReplayReader
try
{
- var bigPath = _mapsToBigs[path];
- big = new BigArchive(bigPath);
- if(big.GetEntry(path) == null)
- {
- //_cache.Remove("bigsCache");
- big.Dispose();
- big = null;
- return false;
- }
+ bigEntry = fs.OpenStream(path, VirtualFileModeType.Open);
+ return true;
}
catch (Exception exception)
{
Debug.Instance.DebugMessage += $"Exception during query (entryStream) of BigMinimapCache: \r\n{exception}\r\n";
- big = null;
- return false;
}
- return true;
+ return false;
}
- public byte[] TryReadBytesFromBig(string path)
+ private void Initialize(string? ra3Directory)
{
- if(_mapsToBigs == null)
- {
- return null;
- }
-
- if(!_mapsToBigs.ContainsKey(path))
- {
- return null;
- }
-
try
{
- var bigPath = _mapsToBigs[path];
- using (var big = new BigArchive(path))
+ if (ra3Directory is null || !Directory.Exists(ra3Directory))
{
- var entry = big.GetEntry(path);
- using (var stream = entry.Open())
- using (var reader = new BinaryReader(stream))
- {
- return reader.ReadBytes((int)entry.Length);
- }
+ Debug.Instance.DebugMessage += $"Will not initialize BigMinimapCache because RA3Directory {ra3Directory} does not exist.\r\n";
+ return;
}
+
+ var currentLanguage = RegistryUtils.RetrieveInRa3(RegistryHive.CurrentUser, "Language");
+ var currentLanguage_ = $"{currentLanguage}_";
+ double SkudefVersionSelector(string fullPath)
+ {
+ var value = -1.0;
+ try
+ {
+ const string skudefPrefix = "RA3";
+ const int prefix = 1;
+ const int language = 2;
+ const int majorVersion = 3;
+ const int minorVersion = 4;
+ const int majorVersionMultiplier = 10000;
+ const int correctLanguageBonus = 1000_0000;
+
+ value = 0;
+ var stem = Path.GetFileNameWithoutExtension(fullPath);
+ var match = Regex.Match(stem, @"([^_]*)_([^0-9]*)([0-9]*)\.([0-9]*)");
+ if (!match.Success || match.Groups.Cast().Any(g => !g.Success))
+ {
+ return value;
+ }
+
+ value += match.Groups[prefix].Value == skudefPrefix ? 0.1 : 0;
+ value += match.Groups[language].Value == currentLanguage_ ? correctLanguageBonus : 0;
+ value += int.Parse(match.Groups[majorVersion].Value) * majorVersionMultiplier;
+ value += int.Parse(match.Groups[minorVersion].Value);
+ return value;
+ }
+ catch (Exception e)
+ {
+ Debug.Instance.DebugMessage += $"Failed to retrieve skudef: {e}\r\n";
+ }
+ return value;
+ }
+ var highestSkudef = (from p in Directory.EnumerateFiles(ra3Directory, "*.SkuDef")
+ orderby SkudefVersionSelector(p) descending
+ select p).First();
+ Debug.Instance.DebugMessage += $"Retrieved highest skudef: {highestSkudef}\r\n";
+
+ using var locker = new Lock(_lock);
+ DronePlatform.BuildTechnologyAssembler();
+ _skudefFileSystem = new SkuDefFileSystemProvider("config", highestSkudef);
}
catch (Exception exception)
{
- Debug.Instance.DebugMessage += $"Exception during query (bytes) of BigMinimapCache: \r\n{exception}\r\n";
- //_cache.Remove("bigsCache");
+ Debug.Instance.DebugMessage += $"Exception during initialization of BigMinimapCache: \r\n{exception}\r\n";
}
-
- return null;
}
}
}
diff --git a/Cache.cs b/Cache.cs
index d70d352..1365cf2 100644
--- a/Cache.cs
+++ b/Cache.cs
@@ -2,11 +2,8 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
-using System.Linq;
-using System.Text;
-using System.Web.Script.Serialization;
using System.Threading.Tasks;
-
+using static System.Text.Json.JsonSerializer;
namespace AnotherReplayReader
{
@@ -15,24 +12,32 @@ namespace AnotherReplayReader
public static string CacheDirectory => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "RA3Bar.Lanyi.AnotherReplayReader");
public static string CacheFilePath => Path.Combine(CacheDirectory, "AnotherReplayReader.cache");
- private ConcurrentDictionary _storage;
+ private readonly ConcurrentDictionary _storage = new();
public Cache()
{
- try
+ Task.Run(async () =>
{
- if (!Directory.Exists(CacheDirectory))
+ try
{
- Directory.CreateDirectory(CacheDirectory);
- }
+ if (!Directory.Exists(CacheDirectory))
+ {
+ Directory.CreateDirectory(CacheDirectory);
+ }
- var serializer = new JavaScriptSerializer();
- _storage = serializer.Deserialize>(File.ReadAllText(CacheFilePath));
- }
- catch
- {
- _storage = new ConcurrentDictionary();
- }
+ using var cacheStream = File.OpenRead(CacheFilePath);
+ var futureCache = DeserializeAsync>(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(string key, in T defaultValue)
@@ -41,8 +46,7 @@ namespace AnotherReplayReader
{
if (_storage.TryGetValue(key, out var valueString))
{
- var serializer = new JavaScriptSerializer();
- return serializer.Deserialize(valueString);
+ return Deserialize(valueString) ?? defaultValue;
}
}
catch { }
@@ -53,8 +57,20 @@ namespace AnotherReplayReader
{
try
{
- var serializer = new JavaScriptSerializer();
- _storage[key] = serializer.Serialize(value);
+ _storage[key] = Serialize(value);
+ Save();
+ }
+ catch { }
+ }
+
+ public void SetValues(params (string Key, object Value)[] values)
+ {
+ try
+ {
+ foreach (var (key, value) in values)
+ {
+ _storage[key] = Serialize(value);
+ }
Save();
}
catch { }
@@ -66,12 +82,12 @@ namespace AnotherReplayReader
Save();
}
- public void Save()
+ public async void Save()
{
try
{
- var serializer = new JavaScriptSerializer();
- File.WriteAllText(CacheFilePath, serializer.Serialize(_storage));
+ using var cacheStream = File.Open(CacheFilePath, FileMode.Create, FileAccess.Write);
+ await SerializeAsync(cacheStream, _storage).ConfigureAwait(false);
}
catch { }
}
diff --git a/Debug.xaml.cs b/Debug.xaml.cs
index 25d2917..017c47d 100644
--- a/Debug.xaml.cs
+++ b/Debug.xaml.cs
@@ -1,7 +1,9 @@
-using Microsoft.Win32;
+using AnotherReplayReader.Utils;
+using Microsoft.Win32;
using System;
+using System.Collections.Generic;
+using System.ComponentModel;
using System.IO;
-using System.Threading;
using System.Windows;
namespace AnotherReplayReader
@@ -21,11 +23,33 @@ namespace AnotherReplayReader
}
}
+ private readonly object _lock = new();
+ private readonly List _list = new();
+
public event Action? NewText;
public Proxy DebugMessage
{
- get => new Proxy();
- set => NewText?.Invoke(value.Payload);
+ get => new();
+ set
+ {
+ var text = value.Payload;
+ using var locker = new Lock(_lock);
+ if (NewText is null || _list.Count > 0)
+ {
+ _list.Add(text);
+ if (NewText is not null)
+ {
+ text = string.Join(string.Empty, _list);
+ _list.Clear();
+ _list.Capacity = 0;
+ }
+ else
+ {
+ return;
+ }
+ }
+ NewText(text);
+ }
}
public event Action? RequestedSave;
@@ -36,18 +60,31 @@ namespace AnotherReplayReader
///
public sealed partial class Debug : Window
{
- public static readonly DebugMessageWrapper Instance = new DebugMessageWrapper();
+ public static readonly DebugMessageWrapper Instance = new();
+ private static readonly object _lock = new();
private static Debug? _window = null;
public static void Initialize()
{
- Interlocked.CompareExchange(ref _window, new(), null);
- _window.InitializeComponent();
+ using var locker = new Lock(_lock);
+ if (_window is not null)
+ {
+ return;
+ }
+ var window = new Debug();
+ window.InitializeComponent();
+ _window = window;
Instance.NewText += _window.AppendText;
Instance.RequestedSave += _window.ExportInMainWindow;
}
- public static new void ShowDialog() => (_window as Window)?.ShowDialog();
+ public static new void ShowDialog() => Lock.Run(_lock, () => (_window as Window)?.ShowDialog());
+
+ protected override void OnClosing(CancelEventArgs e)
+ {
+ e.Cancel = true;
+ Hide();
+ }
private Debug() { }
@@ -55,7 +92,7 @@ namespace AnotherReplayReader
private void OnClear_Click(object sender, RoutedEventArgs e) => _textBox.Clear();
- private void AppendText(string s) => Dispatcher.Invoke(() => _textBox.AppendText(s));
+ private void AppendText(string s) => Dispatcher.InvokeAsync(() => _textBox.AppendText(s));
private void ExportInMainWindow() => Export(Application.Current.MainWindow);
@@ -72,11 +109,9 @@ namespace AnotherReplayReader
var result = saveFileDialog.ShowDialog(owner);
if (result == true)
{
- using (var file = saveFileDialog.OpenFile())
- using (var writer = new StreamWriter(file))
- {
- writer.Write(_textBox.Text);
- }
+ using var file = saveFileDialog.OpenFile();
+ using var writer = new StreamWriter(file);
+ writer.Write(_textBox.Text);
}
});
}
diff --git a/DronePlatform.cs b/DronePlatform.cs
new file mode 100644
index 0000000..2332eb6
--- /dev/null
+++ b/DronePlatform.cs
@@ -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;
+ }
+ }
+}
diff --git a/Excludes.txt b/Excludes.txt
deleted file mode 100644
index 7047d78..0000000
--- a/Excludes.txt
+++ /dev/null
@@ -1 +0,0 @@
-Veldrid\.SDL2\.dll
\ No newline at end of file
diff --git a/ILMergeConfig.json b/ILMergeConfig.json
index 4220560..8ad6429 100644
--- a/ILMergeConfig.json
+++ b/ILMergeConfig.json
@@ -1,121 +1,36 @@
-{
+{
"General": {
+ "OutputFile": null,
+ "TargetPlatform": null,
+ "KeyFile": null,
+ "AlternativeILMergePath": null,
"InputAssemblies": [
- "$(TargetDir)Microsoft.DotNet.PlatformAbstractions.dll",
- "$(TargetDir)Microsoft.Extensions.DependencyModel.dll",
- "$(TargetDir)Microsoft.Win32.Primitives.dll",
- "$(TargetDir)NativeLibraryLoader.dll",
- "$(TargetDir)netstandard.dll",
- "$(TargetDir)Newtonsoft.Json.dll",
- "$(TargetDir)NLog.dll",
- "$(TargetDir)OpenSage.Core.dll",
- "$(TargetDir)OpenSage.FileFormats.Big.dll",
- "$(TargetDir)OpenSage.FileFormats.dll",
- "$(TargetDir)OpenSage.FileFormats.RefPack.dll",
- "$(TargetDir)OpenSage.Mathematics.dll",
- "$(TargetDir)Pfim.dll",
- "$(TargetDir)System.AppContext.dll",
- "$(TargetDir)System.Collections.Concurrent.dll",
- "$(TargetDir)System.Collections.dll",
- "$(TargetDir)System.Collections.NonGeneric.dll",
- "$(TargetDir)System.Collections.Specialized.dll",
- "$(TargetDir)System.ComponentModel.dll",
- "$(TargetDir)System.ComponentModel.EventBasedAsync.dll",
- "$(TargetDir)System.ComponentModel.Primitives.dll",
- "$(TargetDir)System.ComponentModel.TypeConverter.dll",
- "$(TargetDir)System.Console.dll",
- "$(TargetDir)System.Data.Common.dll",
- "$(TargetDir)System.Diagnostics.Contracts.dll",
- "$(TargetDir)System.Diagnostics.Debug.dll",
- "$(TargetDir)System.Diagnostics.DiagnosticSource.dll",
- "$(TargetDir)System.Diagnostics.FileVersionInfo.dll",
- "$(TargetDir)System.Diagnostics.Process.dll",
- "$(TargetDir)System.Diagnostics.StackTrace.dll",
- "$(TargetDir)System.Diagnostics.TextWriterTraceListener.dll",
- "$(TargetDir)System.Diagnostics.Tools.dll",
- "$(TargetDir)System.Diagnostics.TraceSource.dll",
- "$(TargetDir)System.Diagnostics.Tracing.dll",
- "$(TargetDir)System.Drawing.Primitives.dll",
- "$(TargetDir)System.Dynamic.Runtime.dll",
- "$(TargetDir)System.Globalization.Calendars.dll",
- "$(TargetDir)System.Globalization.dll",
- "$(TargetDir)System.Globalization.Extensions.dll",
- "$(TargetDir)System.IO.Compression.dll",
- "$(TargetDir)System.IO.Compression.ZipFile.dll",
- "$(TargetDir)System.IO.dll",
- "$(TargetDir)System.IO.FileSystem.dll",
- "$(TargetDir)System.IO.FileSystem.DriveInfo.dll",
- "$(TargetDir)System.IO.FileSystem.Primitives.dll",
- "$(TargetDir)System.IO.FileSystem.Watcher.dll",
- "$(TargetDir)System.IO.IsolatedStorage.dll",
- "$(TargetDir)System.IO.MemoryMappedFiles.dll",
- "$(TargetDir)System.IO.Pipes.dll",
- "$(TargetDir)System.IO.UnmanagedMemoryStream.dll",
- "$(TargetDir)System.Linq.dll",
- "$(TargetDir)System.Linq.Expressions.dll",
- "$(TargetDir)System.Linq.Parallel.dll",
- "$(TargetDir)System.Linq.Queryable.dll",
- "$(TargetDir)System.Net.Http.dll",
- "$(TargetDir)System.Net.NameResolution.dll",
- "$(TargetDir)System.Net.NetworkInformation.dll",
- "$(TargetDir)System.Net.Ping.dll",
- "$(TargetDir)System.Net.Primitives.dll",
- "$(TargetDir)System.Net.Requests.dll",
- "$(TargetDir)System.Net.Security.dll",
- "$(TargetDir)System.Net.Sockets.dll",
- "$(TargetDir)System.Net.WebHeaderCollection.dll",
- "$(TargetDir)System.Net.WebSockets.Client.dll",
- "$(TargetDir)System.Net.WebSockets.dll",
- "$(TargetDir)System.Numerics.Vectors.dll",
- "$(TargetDir)System.ObjectModel.dll",
- "$(TargetDir)System.Reflection.dll",
- "$(TargetDir)System.Reflection.Extensions.dll",
- "$(TargetDir)System.Reflection.Primitives.dll",
- "$(TargetDir)System.Resources.Reader.dll",
- "$(TargetDir)System.Resources.ResourceManager.dll",
- "$(TargetDir)System.Resources.Writer.dll",
- "$(TargetDir)System.Runtime.CompilerServices.Unsafe.dll",
- "$(TargetDir)System.Runtime.CompilerServices.VisualC.dll",
- "$(TargetDir)System.Runtime.dll",
- "$(TargetDir)System.Runtime.Extensions.dll",
- "$(TargetDir)System.Runtime.Handles.dll",
- "$(TargetDir)System.Runtime.InteropServices.dll",
- "$(TargetDir)System.Runtime.InteropServices.RuntimeInformation.dll",
- "$(TargetDir)System.Runtime.Numerics.dll",
- "$(TargetDir)System.Runtime.Serialization.Formatters.dll",
- "$(TargetDir)System.Runtime.Serialization.Json.dll",
- "$(TargetDir)System.Runtime.Serialization.Primitives.dll",
- "$(TargetDir)System.Runtime.Serialization.Xml.dll",
- "$(TargetDir)System.Security.Claims.dll",
- "$(TargetDir)System.Security.Cryptography.Algorithms.dll",
- "$(TargetDir)System.Security.Cryptography.Csp.dll",
- "$(TargetDir)System.Security.Cryptography.Encoding.dll",
- "$(TargetDir)System.Security.Cryptography.Primitives.dll",
- "$(TargetDir)System.Security.Cryptography.X509Certificates.dll",
- "$(TargetDir)System.Security.Principal.dll",
- "$(TargetDir)System.Security.SecureString.dll",
- "$(TargetDir)System.Text.Encoding.CodePages.dll",
- "$(TargetDir)System.Text.Encoding.dll",
- "$(TargetDir)System.Text.Encoding.Extensions.dll",
- "$(TargetDir)System.Text.RegularExpressions.dll",
- "$(TargetDir)System.Threading.dll",
- "$(TargetDir)System.Threading.Overlapped.dll",
- "$(TargetDir)System.Threading.Tasks.dll",
- "$(TargetDir)System.Threading.Tasks.Parallel.dll",
- "$(TargetDir)System.Threading.Thread.dll",
- "$(TargetDir)System.Threading.ThreadPool.dll",
- "$(TargetDir)System.Threading.Timer.dll",
- "$(TargetDir)System.ValueTuple.dll",
- "$(TargetDir)System.Xml.ReaderWriter.dll",
- "$(TargetDir)System.Xml.XDocument.dll",
- "$(TargetDir)System.Xml.XmlDocument.dll",
- "$(TargetDir)System.Xml.XmlSerializer.dll",
- "$(TargetDir)System.Xml.XPath.dll",
- "$(TargetDir)System.Xml.XPath.XDocument.dll"
+ "NPinyin.Core.dll",
+ "Pfim.dll",
+ "TechnologyAssembler.Core.dll"
]
},
"Advanced": {
- "AllowWildCards": true
+ "AllowDuplicateType": null,
+ "AllowMultipleAssemblyLevelAttributes": false,
+ "AllowWildCards": false,
+ "AllowZeroPeKind": false,
+ "AttributeFile": null,
+ "Closed": false,
+ "CopyAttributes": false,
+ "DebugInfo": true,
+ "DelaySign": false,
+ "DeleteCopiesOverwriteTarget": false,
+ "ExcludeFile": "",
+ "FileAlignment": 512,
+ "Internalize": false,
+ "Log": false,
+ "LogFile": null,
+ "PublicKeyTokens": true,
+ "SearchDirectories": [],
+ "TargetKind": null,
+ "UnionMerge": false,
+ "Version": null,
+ "XmlDocumentation": false
}
-}
-
+}
\ No newline at end of file
diff --git a/MainWindow.xaml b/MainWindow.xaml
index 4051df8..b3204a8 100644
--- a/MainWindow.xaml
+++ b/MainWindow.xaml
@@ -5,7 +5,8 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:AnotherReplayReader"
mc:Ignorable="d"
- Title="MainWindow" Width="800" Height="600">
+ Title="MainWindow" Width="800" Height="600"
+ Loaded="OnMainWindowLoaded">
@@ -71,7 +72,8 @@
-
+
diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs
index 2be3209..4811415 100644
--- a/MainWindow.xaml.cs
+++ b/MainWindow.xaml.cs
@@ -1,6 +1,9 @@
-using Microsoft.Win32;
+using AnotherReplayReader.ReplayFile;
+using AnotherReplayReader.Utils;
+using Microsoft.Win32;
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
@@ -17,28 +20,14 @@ namespace AnotherReplayReader
{
public MainWindowProperties()
{
- string userDataLeafName = null;
- string replayFolderName = null;
- try
- {
- using (var view32 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32))
- using (var ra3Key = view32.OpenSubKey(@"Software\Electronic Arts\Electronic Arts\Red Alert 3", false))
- {
- RA3Directory = ra3Key?.GetValue("Install Dir") as string;
- userDataLeafName = ra3Key?.GetValue("UserDataLeafName") as string;
- replayFolderName = ra3Key?.GetValue("ReplayFolderName") as string;
- }
- }
- catch (Exception e)
- {
- Debug.Instance.DebugMessage += $"获取注册表项时出现错误:{e}\r\n";
- }
-
+ const RegistryHive hklm = RegistryHive.LocalMachine;
+ RA3Directory = RegistryUtils.RetrieveInRa3(hklm, "Install Dir");
+ string? userDataLeafName = RegistryUtils.RetrieveInRa3(hklm, "UserDataLeafName");
+ string? replayFolderName = RegistryUtils.RetrieveInRa3(hklm, "ReplayFolderName");
if (string.IsNullOrWhiteSpace(userDataLeafName))
{
userDataLeafName = "Red Alert 3";
}
-
if (string.IsNullOrWhiteSpace(replayFolderName))
{
replayFolderName = "Replays";
@@ -50,23 +39,21 @@ namespace AnotherReplayReader
ModsDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), userDataLeafName, "Mods");
}
- public event PropertyChangedEventHandler PropertyChanged;
+ public event PropertyChangedEventHandler? PropertyChanged;
// This method is called by the Set accessor of each property.
// The CallerMemberName attribute that is applied to the optional propertyName
// parameter causes the property name of the caller to be substituted as an argument.
private void SetAndNotifyPropertyChanged(ref T target, T newValue, [CallerMemberName] string propertyName = "")
{
- var equals = Equals(target, newValue);
- target = newValue;
- if (!equals)
+ if (!Equals(target, newValue))
{
+ target = newValue;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
-
}
- public string RA3Directory { get; }
+ public string? RA3Directory { get; }
public string RA3ReplayFolderPath { get; }
public string RA3Exe => Path.Combine(RA3Directory, "RA3.exe");
public string CustomMapsDirectory { get; }
@@ -78,19 +65,19 @@ namespace AnotherReplayReader
set => SetAndNotifyPropertyChanged(ref _replayFolderPath, value);
}
- public string ReplayDetails
+ public string? ReplayDetails
{
get => _replayDetails;
set => SetAndNotifyPropertyChanged(ref _replayDetails, value);
}
- public bool ReplaySelected => _currentReplay != null;
+ public bool ReplaySelected => CurrentReplay != null;
- public bool ReplayPlayable => _currentReplay?.HasFooter == true && _currentReplay?.HasCommentator == true && (RA3Directory != null) && File.Exists(RA3Exe);
+ public bool ReplayPlayable => CurrentReplay?.HasFooter == true && CurrentReplay?.HasCommentator == true && (RA3Directory != null) && File.Exists(RA3Exe);
- public bool ReplayDamaged => _currentReplay?.HasFooter == false;
+ public bool ReplayDamaged => CurrentReplay?.HasFooter == false;
- public Replay CurrentReplay
+ public Replay? CurrentReplay
{
get => _currentReplay;
set
@@ -103,9 +90,9 @@ namespace AnotherReplayReader
}
}
- private string _replayFolderPath;
- private string _replayDetails;
- private Replay _currentReplay;
+ private string _replayFolderPath = null!;
+ private string? _replayDetails;
+ private Replay? _currentReplay;
}
///
@@ -113,346 +100,236 @@ namespace AnotherReplayReader
///
public partial class MainWindow : Window
{
- private readonly MainWindowProperties _properties = new MainWindowProperties();
- private readonly Cache _cache = new Cache();
+ private readonly TaskQueue _taskQueue;
+ private readonly MainWindowProperties _properties = new();
+ private readonly Cache _cache = new();
private readonly PlayerIdentity _playerIdentity;
private readonly BigMinimapCache _minimapCache;
private readonly MinimapReader _minimapReader;
- private List _replayList = new List();
- private List _pinyinList = new List();
- private CancellationTokenSource _loadReplaysToken;
+ private readonly CancelManager _cancelLoadReplays = new();
+ private readonly CancelManager _cancelFilterReplays = new();
+ private readonly CancelManager _cancelDisplayReplays = new();
+ private ReplayPinyinList _replayList;
+ private ImmutableArray _filterStrings = ImmutableArray.Empty;
+ private ImmutableArray _filteredReplays = ImmutableArray.Empty;
public MainWindow()
{
- DataContext = _properties;
-
- var handling = new bool[1] { false };
- Application.Current.Dispatcher.UnhandledException += (sender, eventArgs) =>
- {
- if (handling == null || handling[0])
- {
- return;
- }
- handling[0] = true;
- Dispatcher.Invoke(() => MessageBox.Show($"错误:\r\n{eventArgs.Exception}"));
- };
-
+ _taskQueue = new(Dispatcher);
_playerIdentity = new PlayerIdentity(_cache);
- _minimapCache = new BigMinimapCache(_cache, _properties.RA3Directory);
- _minimapReader = new MinimapReader(_minimapCache, _properties.RA3Directory, _properties.CustomMapsDirectory, _properties.ModsDirectory);
+ _minimapCache = new BigMinimapCache(_properties.RA3Directory);
+ _minimapReader = new MinimapReader(_minimapCache, _properties.CustomMapsDirectory, _properties.ModsDirectory);
+ _replayList = new(_playerIdentity);
+
+ DataContext = _properties;
InitializeComponent();
- Closing += (sender, eventArgs) => _cache.Save();
-
- LoadReplays();
- Task.Run(() => AutoSaveReplays(Dispatcher, _properties.RA3ReplayFolderPath));
+ Closing += (sender, eventArgs) =>
+ {
+ _cache.Save();
+ Application.Current.Shutdown();
+ };
}
- private static async Task AutoSaveReplays(Dispatcher dispatcher, string replayFolderPath)
+ private async void OnMainWindowLoaded(object sender, EventArgs e)
{
- const string ourPrefix = "自动保存";
- var errorMessageCount = 0;
- // filename and last write time
- var previousFiles = new Dictionary(StringComparer.OrdinalIgnoreCase);
- // filename and file size
- var lastReplays = new Dictionary(StringComparer.OrdinalIgnoreCase);
-
- while (true)
- {
- try
- {
- var changed = (from fileName in Directory.GetFiles(replayFolderPath, "*.RA3Replay")
- let info = new FileInfo(fileName)
- where !info.Name.StartsWith(ourPrefix)
- where !previousFiles.ContainsKey(info.FullName) || previousFiles[info.FullName] != info.LastWriteTimeUtc
- select info).ToList();
-
- foreach (var info in changed)
- {
- previousFiles[info.FullName] = info.LastWriteTimeUtc;
- }
-
- var replays = changed.Select(info =>
- {
- Debug.Instance.DebugMessage += $"正在尝试检测已更改的文件:{info.FullName}\r\n";
- try
- {
- using (var stream = info.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
- {
- return new Replay(info.FullName, stream);
- }
- }
- catch (Exception e)
- {
- Debug.Instance.DebugMessage += $"自动保存录像/检测录像更改时发生错误:{e}\r\n";
- return null;
- }
- }).Where(replay => replay != null);
-
- var newLastReplays = from replay in replays
- let threshold = Math.Abs((DateTime.UtcNow - replay.Date).TotalSeconds)
- let endDate = replay.Date.Add(replay.Length ?? TimeSpan.Zero)
- let endThreshold = Math.Abs((DateTime.UtcNow - endDate).TotalSeconds)
- where threshold < 40 || endThreshold < 40
- select replay;
-
- var toBeChecked = newLastReplays.ToDictionary(replay => replay.Path, StringComparer.OrdinalIgnoreCase);
- foreach (var savedLastReplay in lastReplays.Keys)
- {
- if (!toBeChecked.ContainsKey(savedLastReplay))
- {
- try
- {
- using (var stream = File.Open(savedLastReplay, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
- {
- toBeChecked.Add(savedLastReplay, new Replay(savedLastReplay, stream));
- }
- }
- catch (Exception e)
- {
- Debug.Instance.DebugMessage += $"自动保存录像/检测录像更改时发生错误:{e}\r\n";
- }
- }
- }
-
- foreach (var kv in toBeChecked)
- {
- Debug.Instance.DebugMessage += $"正在检测录像更改:{kv.Key}\r\n";
- var replay = kv.Value;
- if (lastReplays.TryGetValue(kv.Key, out var fileSize))
- {
- if (fileSize == replay.Size)
- {
- // skip if size is not changed
- Debug.Instance.DebugMessage += $"已跳过未更改的录像:{kv.Key}\r\n";
- continue;
- }
- }
- Debug.Instance.DebugMessage += $"将会自动保存已更改的录像:{kv.Key}\r\n";
- lastReplays[kv.Key] = replay.Size;
-
- var date = replay.Date;
-
- var playerString = $"{replay.NumberOfPlayingPlayers}名玩家";
- if (replay.NumberOfPlayingPlayers <= 2)
- {
- var playingPlayers = from player in replay.Players
- let faction = ModData.GetFaction(replay.Mod, player.FactionID)
- where faction.Kind != FactionKind.Observer
- select $"{player.PlayerName}({faction.Name})";
- playerString = playingPlayers.Aggregate(string.Empty, (x, y) => x + y);
- }
-
- var dateString = $"{date.Year}{date.Month:D2}{date.Day:D2}_{date.Hour:D2}{date.Minute:D2}{date.Second:D2}";
- var destinationPath = Path.Combine(replayFolderPath, $"{ourPrefix}-{playerString}{dateString}.RA3Replay");
- try
- {
- File.Copy(replay.Path, destinationPath, true);
- }
- catch (Exception e)
- {
- throw new Exception($"复制文件({replay.Path} -> {destinationPath})失败:{e.Message}", e);
- }
- }
- }
- catch (Exception e)
- {
- var errorString = $"自动保存录像时出现错误:\r\n{e}\r\n";
- Debug.Instance.DebugMessage += errorString;
- if (Interlocked.Increment(ref errorMessageCount) == 1)
- {
- _ = dispatcher.InvokeAsync(() =>
- {
- try
- {
- MessageBox.Show(errorString);
- }
- finally
- {
- Interlocked.Decrement(ref errorMessageCount);
- }
- });
- }
- }
-
- await Task.Delay(10 * 1000);
- }
+ Debug.Initialize();
+ ReplayAutoSaver.SpawnAutoSaveReplaysTask(_properties.RA3ReplayFolderPath);
+ var token = _cancelLoadReplays.ResetAndGetToken(CancellationToken.None);
+ await _taskQueue.Enqueue(() => LoadReplays(null, token), token);
}
- private async void LoadReplays(string nextSelected = null)
+ private async Task LoadReplays(string? nextSelected, CancellationToken cancelToken)
{
- const string loadingString = "正在加载录像列表,请稍候… 已加载 {0} 个录像";
+ if (!IsLoaded)
+ {
+ return;
+ }
+ cancelToken.ThrowIfCancellationRequested();
+ var filterToken = _cancelFilterReplays.ResetAndGetToken(cancelToken);
+ _cancelDisplayReplays.Reset(_cancelFilterReplays.Token);
- try
- {
- _loadReplaysToken?.Cancel();
- }
- catch (AggregateException e)
- {
- Debug.Instance.DebugMessage += $"Cancellation failed: {e}";
- }
- _loadReplaysToken?.Dispose();
- _loadReplaysToken = new CancellationTokenSource();
- var cancelToken = _loadReplaysToken.Token;
- var path = _properties.ReplayFolderPath;
-
- if (_image != null)
- {
- _image.Source = null;
- }
- if (_dataGrid != null)
+ _properties.CurrentReplay = null;
+ _image.Source = null;
+ if (_dataGrid.Items.Count > 0)
{
_dataGrid.ItemsSource = Array.Empty();
_dataGrid.Items.Refresh();
}
- _replayList.Clear();
- _pinyinList.Clear();
+ cancelToken.ThrowIfCancellationRequested();
+ _replayList = new(_playerIdentity);
+
+ var path = _properties.ReplayFolderPath;
if (!Directory.Exists(path))
{
- DisplayReplays("这个文件夹并不存在。", nextSelected);
+ await FilterReplays("这个文件夹并不存在。", nextSelected, filterToken);
return;
}
- var (newList, newPinyinList) = await Task.Run(() =>
+ var result = await Task.Run(() =>
{
var list = new List();
- var pinyinList = new List();
+ var clock = new Stopwatch();
+ clock.Start();
foreach (var replayPath in Directory.EnumerateFiles(path, "*.RA3Replay"))
{
- if (cancelToken.IsCancellationRequested)
- {
- break;
- }
+ cancelToken.ThrowIfCancellationRequested();
try
{
var replay = new Replay(replayPath);
list.Add(replay);
- pinyinList.Add(replay.ToPinyin(_playerIdentity));
}
catch (Exception exception)
{
Debug.Instance.DebugMessage += $"Uncaught exception when loading replay list: \r\n{exception}\r\n";
continue;
}
- _ = Dispatcher.Invoke(() => _properties.ReplayDetails = string.Format(loadingString, _replayList.Count));
- }
- return (list, pinyinList);
- });
-
- _replayList = newList;
- _pinyinList = newPinyinList;
- DisplayReplays(string.Empty, nextSelected);
- }
-
- private void DisplayReplays(string message = null, string nextSelected = null, Replay[] filtered = null)
- {
- Dispatcher.Invoke(() =>
- {
- filtered = filtered ?? _replayList.ToArray();
- _properties.CurrentReplay = null;
- _dataGrid.ItemsSource = filtered;
- _dataGrid.Items.Refresh();
- _properties.ReplayDetails = message;
-
- if (nextSelected != null)
- {
- for (var i = 0; i < _dataGrid.Items.Count; ++i)
+ if (clock.ElapsedMilliseconds > 300)
{
- if (_dataGrid.Items[i] is Replay replay && replay.Path.Equals(nextSelected, StringComparison.OrdinalIgnoreCase))
- {
- _dataGrid.SelectedIndex = i;
- OnReplaySelectionChanged(null, null);
- break;
- }
+ var text = $"正在加载录像列表,请稍候… 已加载 {list.Count} 个录像";
+ Dispatcher.Invoke(() => _properties.ReplayDetails = text);
+ clock.Restart();
}
}
- });
+ return new ReplayPinyinList(list.ToImmutableArray(), _playerIdentity);
+ }, cancelToken);
+ cancelToken.ThrowIfCancellationRequested();
+ _replayList = result;
+ await FilterReplays(string.Empty, nextSelected, filterToken);
}
- private void OnReplayFolderPathBoxTextChanged(object sender, EventArgs e)
+ private async Task FilterReplays(string message, string? nextSelected, CancellationToken cancelToken)
{
- LoadReplays();
+ if (!IsLoaded)
+ {
+ return;
+ }
+ cancelToken.ThrowIfCancellationRequested();
+ _cancelDisplayReplays.Reset(cancelToken);
+ _filteredReplays = _replayList.Replays;
+
+ _properties.CurrentReplay = null;
+ _dataGrid.SelectedItem = null;
+ _image.Source = null;
+
+ if (_filterStrings.Any())
+ {
+ _properties.ReplayDetails = "正在筛选符合条件的录像…";
+ if (_dataGrid.Items.Count > 0)
+ {
+ _dataGrid.ItemsSource = Array.Empty();
+ _dataGrid.Items.Refresh();
+ }
+
+ var (pinyins, list) = (_filterStrings, _replayList.Pinyins);
+ var result = await Task.Run(() =>
+ {
+ var query = from replay in list.AsParallel().WithCancellation(cancelToken)
+ where pinyins.Any(pinyin => replay.MatchPinyin(pinyin))
+ select replay.Replay;
+ return query.ToImmutableArray();
+ }, cancelToken);
+ cancelToken.ThrowIfCancellationRequested();
+ _filteredReplays = result;
+ }
+
+ _properties.CurrentReplay = null;
+ _dataGrid.SelectedItem = null;
+ _dataGrid.ItemsSource = _filteredReplays;
+ _dataGrid.Items.Refresh();
+ _properties.ReplayDetails = message;
+
+ if (nextSelected is not null)
+ {
+ for (var i = 0; i < _dataGrid.Items.Count; ++i)
+ {
+
+ if (_dataGrid.Items[i] is Replay replay && replay.PathEquals(nextSelected))
+ {
+ _dataGrid.SelectedIndex = i;
+ break;
+ }
+ }
+ }
}
- private void OnReplaySelectionChanged(object sender, EventArgs e)
+ private async Task DisplayReplayDetail(Replay replay, string replayDetails, CancellationToken cancelToken)
{
- _properties.CurrentReplay = _dataGrid.SelectedItem as Replay;
- if (_properties.CurrentReplay == null)
+ if (!IsLoaded)
+ {
+ return;
+ }
+ cancelToken.ThrowIfCancellationRequested();
+ _properties.CurrentReplay = null;
+ _image.Source = null;
+ _properties.ReplayDetails = replayDetails;
+
+ // 开始获取小地图
+ var mapPath = replay.MapPath;
+ var minimapTask = _minimapReader.TryReadTargaAsync(replay);
+
+ // 解析录像内容
+ try
+ {
+ replay = await Task.Run(() => new Replay(replay.Path, true));
+ cancelToken.ThrowIfCancellationRequested();
+ }
+ catch (Exception e) when (e is not OperationCanceledException)
+ {
+ Debug.Instance.DebugMessage += $"Uncaught exception when loading replay body: \r\n{e}\r\n";
+ return;
+ }
+
+ // 假如小地图变了(这……),那么重新加载小地图
+ if (replay.MapPath != mapPath)
+ {
+ minimapTask.Forget();
+ minimapTask = _minimapReader.TryReadTargaAsync(replay);
+ }
+ var newDetails = replay.GetDetails(_playerIdentity);
+ if (_properties.ReplayDetails != newDetails)
+ {
+ if (_replayList.Replays.FindIndex(r => r.PathEquals(replay)) is int index)
+ {
+ _replayList = _replayList.SetItem(index, replay.CloneHeader());
+ var token = _cancelFilterReplays.ResetAndGetToken(_cancelLoadReplays.Token);
+ await FilterReplays(newDetails, replay.Path, token);
+ return;
+ }
+ }
+ _properties.CurrentReplay = replay;
+ _properties.ReplayDetails = newDetails;
+
+ try
+ {
+ var newSource = await minimapTask;
+ cancelToken.ThrowIfCancellationRequested();
+ _image.Source = newSource;
+ /* _image.Width = source.Width;
+ _image.Height = source.Height; */
+ }
+ catch (Exception e) when (e is not OperationCanceledException)
+ {
+ Debug.Instance.DebugMessage += $"Uncaught exception when loading minimap: \r\n{e}\r\n";
+ }
+ }
+
+ private async void OnReplayFolderPathBoxTextChanged(object sender, EventArgs e)
+ {
+ var token = _cancelLoadReplays.ResetAndGetToken(CancellationToken.None);
+ await _taskQueue.Enqueue(() => LoadReplays(null, token), token);
+ }
+
+ private async void OnReplaySelectionChanged(object sender, EventArgs e)
+ {
+ if (_dataGrid.SelectedItem is not Replay replay)
{
return;
}
- Dispatcher.Invoke(() => { _image.Source = null; });
-
- string GetSizeString(double size)
- {
- if (size > 1024 * 1024)
- {
- return $"{Math.Round(size / (1024 * 1024), 2)}MB";
- }
- return $"{Math.Round(size / 1024)}KB";
- }
-
- const string formatA = "文件名:{0}\n大小:{1}\n";
- const string formatB = "地图:{0}\n日期:{1}\n长度:{2}\n";
- const string formatC = "录像类别:{0}\n这个文件是{1}保存的\n";
- const string playerListTitle = "玩家列表:\n";
- var replay = _properties.CurrentReplay;
- var sizeString = GetSizeString(replay.Size);
- var lengthString = "录像已损坏,请先修复录像";
- if (replay.HasFooter)
- {
- lengthString = $"{replay.Length}";
- }
-
- var replaySaver = "[无法获取保存录像的玩家]";
- try
- {
- replaySaver = replay.ReplaySaver.PlayerName;
- }
- catch { }
-
- _properties.ReplayDetails = string.Format(formatA, replay.FileName, sizeString);
- _properties.ReplayDetails += string.Format(formatB, replay.MapName, replay.Date, lengthString);
- _properties.ReplayDetails += string.Format(formatC, replay.TypeString, replaySaver);
- _properties.ReplayDetails += playerListTitle;
- foreach (var player in replay.Players)
- {
- if (player == replay.Players.Last() && player.PlayerName.Equals("post Commentator"))
- {
- break;
- }
- var factionName = ModData.GetFaction(replay.Mod, player.FactionID).Name;
- var realName = replay.Type == ReplayType.Lan ? _playerIdentity.FormatRealName(player.PlayerIP) : string.Empty;
- _properties.ReplayDetails += $"{player.PlayerName + realName},{factionName}\n";
- }
-
- try
- {
- var mapPath = replay.MapPath.TrimEnd('/');
- var mapName = mapPath.Substring(mapPath.LastIndexOf('/') + 1);
- var minimapPath = $"{mapPath}/{mapName}_art.tga";
- Dispatcher.Invoke(() =>
- {
- var source = _minimapReader.TryReadTarga(minimapPath, replay.Mod);
- _image.Source = source;
- /*_image.Width = source.Width;
- _image.Height = source.Height;*/
- });
- }
- catch (Exception exception)
- {
- Debug.Instance.DebugMessage += $"Uncaught exception when loading minimap: \r\n{exception}\r\n";
- }
-
- try
- {
- replay.ParseBody();
- }
- catch (Exception exception)
- {
- Debug.Instance.DebugMessage += $"Uncaught exception when loading replay body: \r\n{exception}\r\n";
- }
+ var token = _cancelDisplayReplays.ResetAndGetToken(_cancelFilterReplays.Token);
+ _properties.ReplayDetails = replay.GetDetails(_playerIdentity);
+ await _taskQueue.Enqueue(() => DisplayReplayDetail(replay, _properties.ReplayDetails, token), token);
}
private void OnAboutButton_Click(object sender, RoutedEventArgs e)
@@ -477,7 +354,6 @@ namespace AnotherReplayReader
var fileName = openFileDialog.FileName;
var directoryName = Path.GetDirectoryName(fileName);
_properties.ReplayFolderPath = directoryName;
- LoadReplays(fileName);
}
}
catch (Exception exception)
@@ -488,7 +364,7 @@ namespace AnotherReplayReader
private void OnDetailsButton_Click(object sender, RoutedEventArgs e)
{
- var detailsWindow = new APM(_properties.CurrentReplay, _playerIdentity);
+ var detailsWindow = new ApmWindow(_properties.CurrentReplay!, _playerIdentity);
detailsWindow.ShowDialog();
}
@@ -499,12 +375,12 @@ namespace AnotherReplayReader
Process.Start(_properties.RA3Exe, $" -replayGame \"{_properties.CurrentReplay}\" ");
}
- private void OnFixReplayButton_Click(object sender, RoutedEventArgs e)
+ private async void OnFixReplayButton_Click(object sender, RoutedEventArgs e)
{
+ var replay = _properties.CurrentReplay ?? throw new InvalidOperationException("Trying to fix a null replay");
+
try
{
- var replay = _properties.CurrentReplay;
-
var saveFileDialog = new SaveFileDialog
{
Filter = "红警3录像文件 (*.RA3Replay)|*.RA3Replay|所有文件 (*.*)|*.*",
@@ -516,11 +392,9 @@ namespace AnotherReplayReader
var result = saveFileDialog.ShowDialog(this);
if (result == true)
{
- using (var file = saveFileDialog.OpenFile())
- using (var writer = new BinaryWriter(file))
- {
- writer.WriteReplay(replay);
- }
+ using var file = saveFileDialog.OpenFile();
+ using var writer = new BinaryWriter(file);
+ writer.Write(replay);
}
}
catch (Exception exception)
@@ -528,42 +402,21 @@ namespace AnotherReplayReader
MessageBox.Show($"无法修复录像:\r\n{exception}");
}
- LoadReplays();
+ var token = _cancelLoadReplays.ResetAndGetToken(CancellationToken.None);
+ await _taskQueue.Enqueue(() => LoadReplays(replay.Path, token), token);
}
private async void ReplayFilterBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
- try
- {
- var pinyins = _replayFilterBox.Text.Split(',', ' ', ',')
- .Select(x => x.ToPinyin())
- .Where(x => !string.IsNullOrEmpty(x))
- .ToArray();
- if (!pinyins.Any())
- {
- DisplayReplays();
- return;
- }
- var tokenSource = _loadReplaysToken;
- var token = tokenSource.Token;
- var list = _pinyinList.ToArray();
- var result = await Task.Run(() =>
- {
- var query = from replay in list.AsParallel()
- where pinyins.Any(pinyin => replay.MatchPinyin(pinyin))
- select replay.Replay;
- return query.ToArray();
- });
- if (token.IsCancellationRequested || _loadReplaysToken != tokenSource)
- {
- return;
- }
- DisplayReplays(null, null, result);
- }
- catch (Exception ex)
- {
- Debug.Instance.DebugMessage += $"Exception when filtering replays: {ex}";
- }
+ _filterStrings = _replayFilterBox.Text
+ .Split(',', ' ', ',')
+ .Select(x => x.ToPinyin())
+ .Where(x => !string.IsNullOrEmpty(x))
+ .ToImmutableArray()!;
+ var currentReplayPath = _properties?.CurrentReplay?.Path;
+
+ var token = _cancelFilterReplays.ResetAndGetToken(_cancelLoadReplays.Token);
+ await _taskQueue.Enqueue(() => FilterReplays(string.Empty, currentReplayPath, token), token);
}
}
}
diff --git a/MinimapReader.cs b/MinimapReader.cs
index 1adef12..b506334 100644
--- a/MinimapReader.cs
+++ b/MinimapReader.cs
@@ -1,13 +1,13 @@
-using System;
+using AnotherReplayReader.ReplayFile;
+using Pfim;
+using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;
using System.Windows.Media.Imaging;
-using OpenSage.FileFormats.Big;
-using Pfim;
+using TechnologyAssembler.Core.IO;
namespace AnotherReplayReader
{
@@ -24,104 +24,93 @@ namespace AnotherReplayReader
};
private readonly BigMinimapCache _cache;
- private readonly string _ra3InstallPath;
private readonly string _mapFolderPath;
private readonly string _modFolderPath;
- public MinimapReader(BigMinimapCache cache, string ra3InstallPath, string mapFolderPath, string modFolderPath)
+ public MinimapReader(BigMinimapCache cache, string mapFolderPath, string modFolderPath)
{
_cache = cache;
- _ra3InstallPath = ra3InstallPath;
_mapFolderPath = mapFolderPath;
_modFolderPath = modFolderPath;
}
- public BitmapSource TryReadTarga(string path, Mod mod, double dpiX = 96.0, double dpiY = 96.0)
+ public Task TryReadTargaAsync(Replay replay, double dpiX = 96.0, double dpiY = 96.0)
{
- using (var targa = TryGetTarga(path, mod))
- {
- if(targa == null)
- {
- return null;
- }
+ var mapPath = replay.MapPath.TrimEnd('/');
+ var mapName = mapPath.Substring(mapPath.LastIndexOf('/') + 1);
+ var minimapPath = $"{mapPath}/{mapName}_art.tga";
+ return Task.Run(() => TryReadTarga(minimapPath, replay.Mod, dpiX, dpiY));
+ }
- try
- {
- return BitmapSource.Create(targa.Width, targa.Height, dpiX, dpiY, FormatMapper[targa.Format], null, targa.Data, targa.Stride);
- }
- catch (Exception exception)
- {
- Debug.Instance.DebugMessage += $"Exception creating BitmapSource from minimap:\r\n {exception}\r\n";
- return null;
- }
+ public BitmapSource? TryReadTarga(string path, Mod mod, double dpiX = 96.0, double dpiY = 96.0)
+ {
+ using var targa = TryGetTarga(path, mod);
+ if (targa == null)
+ {
+ return null;
+ }
+
+ try
+ {
+ var bitmap = BitmapSource.Create(targa.Width, targa.Height, dpiX, dpiY, FormatMapper[targa.Format], null, targa.Data, targa.Stride);
+ bitmap.Freeze();
+ return bitmap;
+ }
+ catch (Exception exception)
+ {
+ Debug.Instance.DebugMessage += $"Exception creating BitmapSource from minimap:\r\n {exception}\r\n";
+ return null;
}
}
- private Targa TryGetTarga(string path, Mod mod)
+ private Targa? TryGetTarga(string path, Mod mod)
{
- var tga = null as Targa;
const string customMapPrefix = "data/maps/internal/";
if (Directory.Exists(_mapFolderPath) && path.StartsWith(customMapPrefix))
{
var minimapPath = Path.Combine(_mapFolderPath, path.Substring(customMapPrefix.Length));
- if(File.Exists(minimapPath))
- {
- return Targa.Create(File.ReadAllBytes(minimapPath), new PfimConfig());
- }
- }
-
- // now, normalize paths
- path = path.Replace('/', '\\');
-
- if(!mod.IsRA3)
- {
- try
- {
- var modSkudefPaths = Enumerable.Empty();
- foreach (var subFolder in Directory.EnumerateDirectories(_modFolderPath))
- {
- modSkudefPaths = modSkudefPaths.Concat(Directory.EnumerateFiles(subFolder, $"{mod.ModName}_{mod.ModVersion}.SkuDef"));
- }
-
- foreach (var modSkudefPath in modSkudefPaths)
- {
- var modBigPaths = BigMinimapCache.ParseSkudefs(new[] { modSkudefPath });
- foreach (var modBigPath in modBigPaths)
- {
- using (var modBig = new BigArchive(modBigPath))
- {
- var entry = modBig.GetEntry(path);
- if (entry != null)
- {
- return Targa.Create(entry.Open(), new PfimConfig());
- }
- }
- }
- }
- }
- catch (Exception exception)
- {
- Debug.Instance.DebugMessage += $"Exception when reading minimap from Skudef big:\r\n {exception}\r\n";
- }
- }
-
- if (_cache != null && _cache.TryGetBigByEntryPath(path, out var big))
- {
- using (big)
- {
- return Targa.Create(big.GetEntry(path).Open(), new PfimConfig());
- }
- }
-
- if (Directory.Exists(_ra3InstallPath))
- {
- var minimapPath = Path.Combine(_mapFolderPath, path);
if (File.Exists(minimapPath))
{
return Targa.Create(File.ReadAllBytes(minimapPath), new PfimConfig());
}
}
+ if (!mod.IsRA3)
+ {
+ var modSkudefPaths = Enumerable.Empty();
+ 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;
}
}
diff --git a/ModData.cs b/ModData.cs
index 8762b4b..9ce0e0e 100644
--- a/ModData.cs
+++ b/ModData.cs
@@ -1,8 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
namespace AnotherReplayReader
{
@@ -42,13 +39,13 @@ namespace AnotherReplayReader
public int CompareTo(object other)
{
- if(!(other is Mod))
+ if (!(other is Mod))
{
return GetType().FullName.CompareTo(other.GetType().FullName);
}
var otherMod = (Mod)other;
- if(IsRA3 != otherMod.IsRA3)
+ if (IsRA3 != otherMod.IsRA3)
{
if (IsRA3)
{
@@ -84,20 +81,13 @@ namespace AnotherReplayReader
internal static class ModData
{
- private static readonly IReadOnlyDictionary _ra3Factions;
- private static readonly IReadOnlyDictionary _ra3ARFactions;
- private static readonly IReadOnlyDictionary _ra3CoronaFactions;
- private static readonly IReadOnlyDictionary _ra3DawnFactions;
- private static readonly IReadOnlyDictionary _ra3INSFactions;
- private static readonly IReadOnlyDictionary _ra3FSFactions;
- private static readonly IReadOnlyDictionary _ra3EisenreichFactions;
- private static readonly IReadOnlyDictionary _ra3TNWFactions;
- private static readonly IReadOnlyDictionary _ra3WOPFactions;
- private static readonly Faction _unknown;
+ private static readonly Faction _unknown = new(FactionKind.Unknown, "未知阵营");
+ private static readonly IReadOnlyDictionary> _factions;
+
static ModData()
{
- _ra3Factions = new Dictionary
+ var ra3Factions = new Dictionary
{
{ 0, new Faction(FactionKind.Player, "AI") },
{ 1, new Faction(FactionKind.Observer, "观察员") },
@@ -106,8 +96,8 @@ namespace AnotherReplayReader
{ 4, new Faction(FactionKind.Player, "盟军") },
{ 7, new Faction(FactionKind.Player, "随机") },
{ 8, new Faction(FactionKind.Player, "苏联") },
- };
- _ra3ARFactions = new Dictionary
+ };
+ var arFactions = new Dictionary
{
{ 1, new Faction(FactionKind.Player, "阳炎") },
{ 2, new Faction(FactionKind.Player, "天琼") },
@@ -123,7 +113,7 @@ namespace AnotherReplayReader
{ 14, new Faction(FactionKind.Player, "涅墨西斯") },
{ 0, new Faction(FactionKind.Player, "AI") },
};
- _ra3CoronaFactions = new Dictionary
+ var coronaFactions = new Dictionary
{
{ 0, new Faction(FactionKind.Player, "AI") },
{ 1, new Faction(FactionKind.Observer, "观察员") },
@@ -134,7 +124,7 @@ namespace AnotherReplayReader
{ 8, new Faction(FactionKind.Player, "苏联") },
{ 9, new Faction(FactionKind.Player, "神州") },
};
- _ra3DawnFactions = new Dictionary
+ var dawnFactions = new Dictionary
{
{ 0, new Faction(FactionKind.Player, "AI") },
{ 1, new Faction(FactionKind.Observer, "观察员") },
@@ -147,7 +137,7 @@ namespace AnotherReplayReader
{ 11, new Faction(FactionKind.Player, "随机") },
{ 12, new Faction(FactionKind.Player, "苏联") },
};
- _ra3INSFactions = new Dictionary
+ var insFactions = new Dictionary
{
{ 0, new Faction(FactionKind.Player, "AI") },
{ 1, new Faction(FactionKind.Observer, "观察员") },
@@ -157,7 +147,7 @@ namespace AnotherReplayReader
{ 7, new Faction(FactionKind.Player, "随机") },
{ 8, new Faction(FactionKind.Player, "苏联") },
};
- _ra3FSFactions = new Dictionary
+ var fsFactions = new Dictionary
{
{ 0, new Faction(FactionKind.Player, "AI") },
{ 1, new Faction(FactionKind.Observer, "观察员") },
@@ -167,7 +157,7 @@ namespace AnotherReplayReader
{ 7, new Faction(FactionKind.Player, "随机") },
{ 8, new Faction(FactionKind.Player, "苏联") },
};
- _ra3EisenreichFactions = new Dictionary
+ var eisenreichFactions = new Dictionary
{
{ 0, new Faction(FactionKind.Player, "AI") },
{ 1, new Faction(FactionKind.Observer, "观察员") },
@@ -177,7 +167,7 @@ namespace AnotherReplayReader
{ 7, new Faction(FactionKind.Player, "随机") },
{ 8, new Faction(FactionKind.Player, "苏联") },
};
- _ra3TNWFactions = new Dictionary
+ var tnwFactions = new Dictionary
{
{ 0, new Faction(FactionKind.Player, "AI") },
{ 1, new Faction(FactionKind.Observer, "观察员") },
@@ -187,7 +177,7 @@ namespace AnotherReplayReader
{ 7, new Faction(FactionKind.Player, "随机") },
{ 8, new Faction(FactionKind.Player, "苏联") },
};
- _ra3WOPFactions = new Dictionary
+ var wopFactions = new Dictionary
{
{ 0, new Faction(FactionKind.Player, "AI") },
{ 1, new Faction(FactionKind.Observer, "观察员") },
@@ -197,61 +187,31 @@ namespace AnotherReplayReader
{ 7, new Faction(FactionKind.Player, "随机") },
{ 8, new Faction(FactionKind.Player, "苏联") },
};
- _unknown = new Faction(FactionKind.Unknown, "未知阵营");
+ _factions = new Dictionary>(StringComparer.CurrentCultureIgnoreCase)
+ {
+ ["RA3"] = ra3Factions,
+ ["Armor Rush"] = arFactions,
+ ["ART"] = arFactions,
+ ["corona"] = coronaFactions,
+ ["Dawn"] = dawnFactions,
+ ["Insurrection"] = insFactions,
+ ["1.12+FS"] = fsFactions,
+ ["Eisenreich"] = eisenreichFactions,
+ ["The New World"] = tnwFactions,
+ ["War Of Powers"] = wopFactions
+ };
}
- public static Faction GetFaction(Mod mod, int factionID)
+ public static Faction GetFaction(Mod mod, int factionId)
{
- new Faction(FactionKind.Player, mod.ModName + "-" + factionID);
-
- if
- (mod.ModName.Equals("RA3"))
+ if (_factions.TryGetValue(mod.ModName, out var table))
{
- return _ra3Factions.TryGetValue(factionID, out var faction) ? faction : _unknown;
+ if (table.TryGetValue(factionId, out var result))
+ {
+ return result;
+ }
}
- if (mod.ModName.Equals("Armor Rush"))
- {
- return _ra3ARFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
- }
- if (mod.ModName.Equals("ART"))
- {
- return _ra3ARFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
- }
- if (mod.ModName.Equals("corona"))
- {
- return _ra3CoronaFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
- }
- if (mod.ModName.Equals("Dawn"))
- {
- return _ra3DawnFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
- }
- if (mod.ModName.Equals("Insurrection"))
- {
- return _ra3INSFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
- }
- if (mod.ModName.Equals("1.12+FS"))
- {
- return _ra3FSFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
- }
- if (mod.ModName.Equals("Eisenreich"))
- {
- return _ra3EisenreichFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
- }
- if (mod.ModName.Equals("The New World"))
- {
- return _ra3TNWFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
- }
- if (mod.ModName.Equals("War Of Powers"))
- {
- return _ra3WOPFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
- }
-
-
-
-
-
return _unknown;
-
}
}
diff --git a/PinyinExtensions.cs b/PinyinExtensions.cs
deleted file mode 100644
index cc9a6af..0000000
--- a/PinyinExtensions.cs
+++ /dev/null
@@ -1,76 +0,0 @@
-using NPinyin;
-using System;
-using System.Collections.Generic;
-
-namespace AnotherReplayReader
-{
- class PinyinReplayData
- {
- public Replay Replay { get; }
- public string Name { get; }
- public string Map { get; }
- public string Mod { get; }
- public List Players { get; } = new List();
- public List RealNames { get; } = new List();
- public List Factions { get; } = new List();
-
- public PinyinReplayData(Replay replay, PlayerIdentity playerIdentity)
- {
- Replay = replay;
- Name = replay.FileName.ToPinyin();
- Map = replay.MapName.ToPinyin();
- Mod = replay.Mod.ModName.ToPinyin();
- foreach (var player in replay.Players)
- {
- void AddIfNotEmpty(List target, string s)
- {
- s = s.ToPinyin();
- if (!string.IsNullOrEmpty(s))
- {
- target.Add(s);
- }
- }
- AddIfNotEmpty(Players, player.PlayerName);
- AddIfNotEmpty(RealNames, playerIdentity.GetRealName(player.PlayerIP));
- AddIfNotEmpty(Factions, ModData.GetFaction(replay.Mod, player.FactionID).Name);
- }
- }
-
- public bool MatchPinyin(string pinyin)
- {
- return Name.ContainsIgnoreCase(pinyin)
- || Players.FindIndex(s => s.ContainsIgnoreCase(pinyin)) != -1
- || RealNames.FindIndex(s => s.ContainsIgnoreCase(pinyin)) != -1
- || Map.ContainsIgnoreCase(pinyin)
- || Mod.ContainsIgnoreCase(pinyin)
- || Factions.FindIndex(s => s.ContainsIgnoreCase(pinyin)) != -1;
- }
- }
-
- static class PinyinExtensions
- {
- public static bool ContainsIgnoreCase(this string self, string s)
- {
- return s != null && self.IndexOf(s, StringComparison.CurrentCultureIgnoreCase) != -1;
- }
-
- public static string ToPinyin(this string self)
- {
- string pinyin;
- try
- {
- pinyin = Pinyin.GetPinyin(self);
- }
- catch
- {
- return null;
- }
- return pinyin.Replace(" ", "");
- }
-
- public static PinyinReplayData ToPinyin(this Replay replay, PlayerIdentity playerIdentity)
- {
- return new PinyinReplayData(replay, playerIdentity);
- }
- }
-}
diff --git a/PlayerIdentity.cs b/PlayerIdentity.cs
index d368fd3..b5cfba1 100644
--- a/PlayerIdentity.cs
+++ b/PlayerIdentity.cs
@@ -1,33 +1,38 @@
-using System;
+using AnotherReplayReader.Utils;
+using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
+using System.Security.Cryptography;
using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
using System.Threading.Tasks;
using System.Web;
-using System.Web.Script.Serialization;
+using static System.Text.Json.JsonSerializer;
namespace AnotherReplayReader
{
- internal sealed class IPAndPlayer
+ internal sealed class IpAndPlayer
{
public static string SimpleIPToString(uint ip)
{
return $"{ip / 256 / 256 / 256}.{ip / 256 / 256 % 256}.{ip / 256 % 256}.{ip % 256}";
}
- public uint IP
+ [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
+ public uint Ip
{
get => _ip;
set
{
_ip = value;
- IPString = SimpleIPToString(_ip);
+ IpString = SimpleIPToString(_ip);
}
}
- public string IPString { get; private set; }
- public string ID
+ public string IpString { get; private set; } = "0.0.0.0";
+ public string Id
{
get => _id;
set
@@ -36,116 +41,88 @@ namespace AnotherReplayReader
_pinyin = _id.ToPinyin();
}
}
- public string PinyinID => _pinyin;
+ public string? PinyinId => _pinyin;
private uint _ip;
- private string _id;
- private string _pinyin;
+ private string _id = string.Empty;
+ private string? _pinyin;
}
internal class PlayerIdentity
{
- public bool IsUsable => IsListUsable(_list);
+ private const string StoredKey = "pt";
+ private const string IvKey = "jw";
+ private static readonly JsonSerializerOptions _jsonOptions = new()
+ {
+ PropertyNameCaseInsensitive = true
+ };
+ private readonly object _lock = new();
+ private readonly Cache _cache;
+ private IReadOnlyDictionary? _list;
- private Cache _cache;
- private volatile IReadOnlyDictionary _list;
+ public bool IsUsable => Lock.Run(_lock, () => _list is not null);
public PlayerIdentity(Cache cache)
{
_cache = cache;
+ Task.Run(FetchLocal);
Fetch();
-
- try
- {
- var stored = _cache.GetOrDefault("pt", string.Empty);
- if (string.IsNullOrWhiteSpace(stored))
- {
- return;
- }
-
- var bytes = Convert.FromBase64String(stored);
- var id = Encoding.UTF8.GetBytes(Auth.ID);
- var data = Encoding.UTF8.GetString(bytes.Select((x, i) => (byte)(x ^ id[i % id.Length])).ToArray());
- var serializer = new JavaScriptSerializer();
- var cachedTable = serializer.Deserialize>(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 list)
- {
- return list != null && list.Count != 0;
}
public Task Fetch()
{
- return Task.Run(() =>
+ return Task.Run(async () =>
{
try
{
var key = HttpUtility.UrlEncode(Auth.GetKey());
var request = WebRequest.Create($"https://lanyi.altervista.org/playertable/playertable.php?do=getTable&key={key}");
-
- using (var stream = request.GetResponse().GetResponseStream())
- using (var reader = new StreamReader(stream))
+ using var response = await request.GetResponseAsync().ConfigureAwait(false);
+ using var stream = response.GetResponseStream();
+ var list = await DeserializeAsync>(stream, _jsonOptions).ConfigureAwait(false);
+ var converted = list?.ToDictionary(x => x.Ip, x => x.Id);
+ lock (_lock)
{
- var response = reader.ReadToEnd();
- var serializer = new JavaScriptSerializer();
- var temp = serializer.Deserialize>(response);
- if (temp == null)
- {
- _list = null;
- return;
- }
- var converted = temp.ToDictionary(x => x.IP, x => x.ID);
_list = converted;
-
- var bytes = Encoding.UTF8.GetBytes(serializer.Serialize(temp));
- var id = Encoding.UTF8.GetBytes(Auth.ID);
- var base64 = Convert.ToBase64String(bytes.Select((x, i) => (byte)(x ^ id[i % id.Length])).ToArray());
- _cache.Set("pt", base64);
}
+
+ if (list is null)
+ {
+ _cache.Set(StoredKey, null);
+ return;
+ }
+
+ using var aes = Aes.Create();
+ using var encryptor = aes.CreateEncryptor(Encoding.UTF8.GetBytes(Auth.ID), aes.IV);
+ using var memory = new MemoryStream();
+ using var decryptorStream = new CryptoStream(memory, encryptor, CryptoStreamMode.Write);
+ await SerializeAsync(decryptorStream, list, _jsonOptions).ConfigureAwait(false);
+ memory.Flush();
+ _cache.SetValues((StoredKey, Convert.ToBase64String(memory.ToArray())), (IvKey, Convert.ToBase64String(aes.IV)));
}
catch { }
});
}
- public List AsSortedList()
+ public List AsSortedList()
{
- var list = _list;
+ using var locker = new Lock(_lock);
- if (!IsListUsable(list))
- {
- return new List();
- }
-
- return _list.Select((kv) => new IPAndPlayer { IP = kv.Key, ID = kv.Value }).OrderBy(x => x.IP).ToList();
+ return _list?
+ .Select((kv) => new IpAndPlayer { Ip = kv.Key, Id = kv.Value })
+ .OrderBy(x => x.Ip)
+ .ToList() ?? new(0);
}
public string GetRealName(uint ip)
{
- var list = _list;
-
- if (!IsListUsable(list))
- {
- return string.Empty;
- }
-
if (ip == 0)
{
return string.Empty;
}
- if (!list.TryGetValue(ip, out var name))
+ using var locker = new Lock(_lock);
+ if (_list is null || !_list.TryGetValue(ip, out var name))
{
return string.Empty;
}
@@ -158,27 +135,50 @@ namespace AnotherReplayReader
var name = GetRealName(ip);
if (string.IsNullOrEmpty(name))
{
- name = IPAndPlayer.SimpleIPToString(ip);
+ name = IpAndPlayer.SimpleIPToString(ip);
}
return $"({name})";
}
public string QueryRealNameAndIP(uint ip)
{
- var list = _list;
-
- if (!IsListUsable(list))
+ var ipText = IpAndPlayer.SimpleIPToString(ip);
+ var name = GetRealName(ip);
+ if (string.IsNullOrEmpty(name))
{
- return string.Empty;
+ return ipText;
}
+ return $"{name},{ipText}";
+ }
- if (ip == 0)
+ private async Task FetchLocal()
+ {
+ try
{
- return string.Empty;
- }
+ var stored = _cache.GetOrDefault(StoredKey, string.Empty);
+ var iv = Convert.FromBase64String(_cache.GetOrDefault(IvKey, string.Empty));
+ if (string.IsNullOrWhiteSpace(stored))
+ {
+ return;
+ }
- var name = list.TryGetValue(ip, out var realName) ? realName + "," : string.Empty;
- return name + IPAndPlayer.SimpleIPToString(ip);
+ using var aes = Aes.Create();
+ using var decryptor = aes.CreateDecryptor(Encoding.UTF8.GetBytes(Auth.ID), iv);
+ using var memory = new MemoryStream(Convert.FromBase64String(stored));
+ using var decryptorStream = new CryptoStream(memory, decryptor, CryptoStreamMode.Read);
+ var cachedTable = await DeserializeAsync>(decryptorStream, _jsonOptions).ConfigureAwait(false);
+ if (cachedTable is null)
+ {
+ return;
+ }
+ var converted = cachedTable.ToDictionary(x => x.Ip, x => x.Id);
+ converted[0] = "【没有网络连接,正在使用上次保存的数据】";
+ lock (_lock)
+ {
+ _list ??= converted;
+ }
+ }
+ catch { }
}
}
}
diff --git a/Replay.cs b/Replay.cs
deleted file mode 100644
index 4a04cee..0000000
--- a/Replay.cs
+++ /dev/null
@@ -1,433 +0,0 @@
-using System;
-using System.IO;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace AnotherReplayReader
-{
- internal sealed class Player
- {
- public static readonly IReadOnlyDictionary ComputerNames = new Dictionary
- {
- { "E", "简单" },
- { "M", "中等" },
- { "H", "困难" },
- { "B", "凶残" },
- };
-
- public string PlayerName { get; private set; }
- public uint PlayerIP { get; private set; }
- public int FactionID { get; private set; }
- public int Team { get; private set; }
-
- public Player(string[] playerEntry)
- {
- var isComputer = playerEntry[0][0] == 'C';
-
- PlayerName = playerEntry[0].Substring(1);
-
- if (isComputer)
- {
- PlayerName = ComputerNames[PlayerName];
- PlayerIP = 0;
- FactionID = int.Parse(playerEntry[2]);
- Team = int.Parse(playerEntry[4]);
- }
- else
- {
- PlayerIP = uint.Parse(playerEntry[1], System.Globalization.NumberStyles.HexNumber);
- FactionID = int.Parse(playerEntry[5]);
- Team = int.Parse(playerEntry[7]);
- }
-
-
- }
- }
-
- internal enum ReplayType
- {
- Skirmish,
- Lan,
- Online
- }
-
- internal sealed class ReplayChunk
- {
- public uint TimeCode { get; private set; }
- public byte Type { get; private set; }
- public byte[] Data { get; private set; }
-
- public ReplayChunk(uint timeCode, BinaryReader reader)
- {
- TimeCode = timeCode; // reader.ReadUInt32();
- Type = reader.ReadByte();
- var chunkSize = reader.ReadInt32();
- Data = reader.ReadBytes(chunkSize);
- if(reader.ReadInt32() != 0)
- {
- throw new InvalidDataException("Replay Chunk not ended with zero");
- }
- }
- }
-
- internal enum ReplayFooterOption
- {
- SeekToFooter,
- CurrentlyAtFooter,
- }
-
- internal sealed class ReplayFooter
- {
- public const uint Terminator = 0x7FFFFFFF;
- public static readonly byte[] FooterString = Encoding.ASCII.GetBytes("RA3 REPLAY FOOTER");
- public uint FinalTimeCode { get; private set; }
- public byte[] Data { get; private set; }
-
- public ReplayFooter(BinaryReader reader, ReplayFooterOption option)
- {
- var currentPosition = reader.BaseStream.Position;
- reader.BaseStream.Seek(-4, SeekOrigin.End);
- var footerLength = reader.ReadInt32();
-
- if (option == ReplayFooterOption.SeekToFooter)
- {
- currentPosition = reader.BaseStream.Length - footerLength;
- }
-
- reader.BaseStream.Seek(currentPosition, SeekOrigin.Begin);
- var footer = reader.ReadBytes(footerLength);
-
- if(footer.Length != footerLength || reader.BaseStream.Position != reader.BaseStream.Length)
- {
- throw new InvalidDataException("Invalid footer");
- }
-
- using (var footerStream = new MemoryStream(footer))
- using (var footerReader = new BinaryReader(footerStream))
- {
- var footerString = footerReader.ReadBytes(17);
- if(!footerString.SequenceEqual(FooterString))
- {
- throw new InvalidDataException("Invalid footer, no footer string");
- }
- FinalTimeCode = footerReader.ReadUInt32();
- Data = footerReader.ReadBytes(footer.Length - 25);
- if(footerReader.ReadInt32() != footerLength)
- {
- throw new InvalidDataException();
- }
- }
- }
-
- public ReplayFooter(uint finalTimeCode)
- {
- FinalTimeCode = finalTimeCode;
- Data = new byte[] { 0x02, 0x1A, 0x00, 0x00, 0x00 };
- }
-
- List TryGetKillDeathRatio()
- {
- if(Data.Length < 24)
- {
- return null;
- }
-
- var ratios = new List();
- 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[] 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 TypeStrings = new Dictionary()
- {
- { 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 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 Body => _body;
- public ReplayFooter Footer { get; private set; }
-
- private List _players;
- private byte _replaySaverIndex;
- private long _headerSize;
- private List _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;
- try
- {
- var query = from splitted in description.Split(';')
- where !string.IsNullOrWhiteSpace(splitted)
- select splitted.Split(new[] { '=' }, 2);
- entries = query.ToDictionary(x => x[0], x => x[1]);
- }
- catch(Exception exception)
- {
- throw new InvalidDataException($"Failed to parse string header of replay {Path}: \r\n{exception}");
- }
-
- try
- {
- _players = entries["S"].Split(':')
- .TakeWhile(x => !string.IsNullOrWhiteSpace(x) && x[0] != 'X')
- .Select(x => new Player(x.Split(',')))
- .ToList();
- }
- catch (Exception exception)
- {
- throw new InvalidDataException($"Failed to parse playerdata from string header of replay {Path}: \r\n{exception}");
- }
-
- MapPath = entries["M"].Substring(3);
-
- HasCommentator = !entries["PC"].Equals("-1");
-
- var lanFlag = int.Parse(entries["GT"]) == 0;
- if (lanFlag)
- {
- if(_players.First().PlayerIP == 0)
- {
- Type = ReplayType.Skirmish;
- }
- else
- {
- Type = ReplayType.Lan;
- }
- }
- else
- {
- Type = ReplayType.Online;
- }
-
- _replaySaverIndex = reader.ReadByte();
-
- reader.ReadBytes(8); // 8 bit paddings
- var fileNameLength = reader.ReadInt32();
- reader.ReadBytes(fileNameLength * 2);
- reader.ReadBytes(16);
- var verMagicLength = reader.ReadInt32();
- reader.ReadBytes(verMagicLength);
- reader.ReadBytes(85);
-
- if(reader.BaseStream.Position != _headerSize)
- {
- throw new InvalidDataException();
- }
-
- reader.BaseStream.Seek(-4, SeekOrigin.End);
- try
- {
- Footer = new ReplayFooter(reader, ReplayFooterOption.SeekToFooter);
- Length = TimeSpan.FromSeconds(Math.Round(Footer.FinalTimeCode / 15.0));
- }
- catch(Exception)
- {
- Length = null;
- Footer = null;
- }
- }
- }
-
- public void ParseBody()
- {
- _body = new List();
-
- 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 GetCommandCounts()
- {
- var playerCommands = new Dictionary();
-
- 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;
- }
- }
-}
diff --git a/ReplayAutoSaver.cs b/ReplayAutoSaver.cs
new file mode 100644
index 0000000..5f4d4bf
--- /dev/null
+++ b/ReplayAutoSaver.cs
@@ -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(StringComparer.OrdinalIgnoreCase);
+ // filename and file size
+ var lastReplays = new Dictionary(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);
+ }
+ }
+ }
+}
diff --git a/CommandChunk.cs b/ReplayFile/CommandChunk.cs
similarity index 77%
rename from CommandChunk.cs
rename to ReplayFile/CommandChunk.cs
index 8a58dee..2a6d0e5 100644
--- a/CommandChunk.cs
+++ b/ReplayFile/CommandChunk.cs
@@ -2,20 +2,13 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-namespace AnotherReplayReader
+namespace AnotherReplayReader.ReplayFile
{
internal static class RA3Commands
{
- //public static IReadOnlyDictionary> CommandParser { get; private set; }
-
- public static IReadOnlyDictionary> CommandParser => _commandParser;
- public static IReadOnlyDictionary CommandNames => _commandNames;
-
- private static Dictionary> _commandParser;
- private static Dictionary _commandNames;
+ private static readonly Dictionary> _commandParser;
+ private static readonly Dictionary _commandNames;
static RA3Commands()
{
@@ -31,13 +24,13 @@ namespace AnotherReplayReader
};
}
- Action variableSizeParser (byte command, int offset)
+ Action variableSizeParser(byte command, int offset)
{
return (BinaryReader current) =>
{
var totalBytes = 2;
totalBytes += current.ReadBytes(offset - 2).Length;
- for(var x = current.ReadByte(); x != 0xFF; x = current.ReadByte())
+ for (var x = current.ReadByte(); x != 0xFF; x = current.ReadByte())
{
totalBytes += 1;
@@ -107,7 +100,7 @@ namespace AnotherReplayReader
(0x02, ParseSetRallyPoint0x02, "设计集结点"),
(0x0C, ParseUngarrison0x0C, "从进驻的建筑撤出(?)"),
(0x10, ParseGarrison0x10, "进驻建筑"),
- (0x33, ParseUUID0x33, "[游戏自动生成的UUID指令]"),
+ (0x33, ParseUuid0x33, "[游戏自动生成的UUID指令]"),
(0x4B, ParsePlaceBeacon0x4B, "信标")
};
@@ -129,9 +122,26 @@ namespace AnotherReplayReader
_commandNames.Add(0x4D, "在信标里输入文字");
}
- public static void UnknownCommandParser(BinaryReader current, byte commandID)
+ public static void ReadCommandData(this BinaryReader reader, byte commandId)
{
- while(true)
+ if (_commandParser.TryGetValue(commandId, out var parser))
+ {
+ _commandParser[commandId](reader);
+ }
+ else
+ {
+ UnknownCommandParser(reader, commandId);
+ }
+ }
+
+ public static string GetCommandName(byte commandId)
+ {
+ return _commandNames.TryGetValue(commandId, out var storedName) ? storedName : $"(未知指令 0x{commandId:2X})";
+ }
+
+ public static void UnknownCommandParser(BinaryReader current, byte commandId)
+ {
+ while (true)
{
var value = current.ReadByte();
if (value == 0xFF)
@@ -143,11 +153,6 @@ namespace AnotherReplayReader
//return $"(未知指令 0x{commandID:2X})";
}
- public static string GetCommandName(byte commandID)
- {
- return CommandNames.TryGetValue(commandID, out var storedName) ? storedName : $"(未知指令 0x{commandID:2X})";
- }
-
private static void ParseSpecialChunk0x01(BinaryReader current)
{
var firstByte = current.ReadByte();
@@ -157,7 +162,7 @@ namespace AnotherReplayReader
}
var sixthByte = current.ReadBytes(5).Last();
- if(sixthByte == 0xFF)
+ if (sixthByte == 0xFF)
{
return;
}
@@ -165,7 +170,7 @@ namespace AnotherReplayReader
var sixteenthByte = current.ReadBytes(10).Last();
var size = (int)(sixteenthByte + 1) * 4 + 14;
var lastByte = current.ReadBytes(size).Last();
- if(lastByte != 0xFF)
+ if (lastByte != 0xFF)
{
throw new InvalidDataException();
}
@@ -198,23 +203,23 @@ namespace AnotherReplayReader
{
var type = current.ReadByte();
var size = -1;
- if(type == 0x14)
+ if (type == 0x14)
{
size = 9;
}
- else if(type == 0x04)
+ else if (type == 0x04)
{
size = 10;
}
var lastByte = current.ReadBytes(size).Last();
- if(lastByte != 0xFF)
+ if (lastByte != 0xFF)
{
throw new InvalidDataException();
}
}
- private static void ParseUUID0x33(BinaryReader current)
+ private static void ParseUuid0x33(BinaryReader current)
{
current.ReadByte();
var firstStringLength = (int)current.ReadByte();
@@ -231,11 +236,11 @@ namespace AnotherReplayReader
{
var type = current.ReadByte();
var size = -1;
- if(type == 0x04)
+ if (type == 0x04)
{
size = 5;
}
- else if(type == 0x07)
+ else if (type == 0x07)
{
size = 13;
}
@@ -250,7 +255,7 @@ namespace AnotherReplayReader
internal sealed class CommandChunk
{
- public byte CommandID { get; private set; }
+ public byte CommandId { get; private set; }
public int PlayerIndex { get; private set; }
public static List Parse(in ReplayChunk chunk)
@@ -260,44 +265,37 @@ namespace AnotherReplayReader
throw new NotImplementedException();
}
- int ManglePlayerID(byte ID)
+ static int ManglePlayerId(byte id)
{
- return ID / 8 - 2;
+ return id / 8 - 2;
}
- using (var stream = new MemoryStream(chunk.Data))
- using (var reader = new BinaryReader(stream))
+ using var reader = chunk.GetReader();
+ if (reader.ReadByte() != 1)
{
- if(reader.ReadByte() != 1)
- {
- throw new InvalidDataException("Payload first byte not 1");
- }
-
- var list = new List();
- var numberOfCommands = reader.ReadInt32();
- for(var i = 0; i < numberOfCommands; ++i)
- {
- var commandID = reader.ReadByte();
- var playerID = reader.ReadByte();
- if (RA3Commands.CommandParser.TryGetValue(commandID, out var parser))
- {
- RA3Commands.CommandParser[commandID](reader);
- }
- else
- {
- RA3Commands.UnknownCommandParser(reader, commandID);
- }
-
- list.Add(new CommandChunk { CommandID = commandID, PlayerIndex = ManglePlayerID(playerID) });
- }
-
- if(reader.BaseStream.Position != reader.BaseStream.Length)
- {
- throw new InvalidDataException("Payload not fully parsed");
- }
-
- return list;
+ throw new InvalidDataException("Payload first byte not 1");
}
+
+ var list = new List();
+ 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;
}
}
}
diff --git a/ReplayFile/Player.cs b/ReplayFile/Player.cs
new file mode 100644
index 0000000..a4f0dc5
--- /dev/null
+++ b/ReplayFile/Player.cs
@@ -0,0 +1,41 @@
+using System.Collections.Generic;
+
+namespace AnotherReplayReader.ReplayFile
+{
+ internal sealed class Player
+ {
+ public static readonly IReadOnlyDictionary ComputerNames = new Dictionary
+ {
+ { "E", "简单" },
+ { "M", "中等" },
+ { "H", "困难" },
+ { "B", "凶残" },
+ };
+
+ public string PlayerName { get; }
+ public uint PlayerIp { get; }
+ public int FactionId { get; }
+ public int Team { get; }
+
+ public Player(string[] playerEntry)
+ {
+ var isComputer = playerEntry[0][0] == 'C';
+
+ PlayerName = playerEntry[0].Substring(1);
+
+ if (isComputer)
+ {
+ PlayerName = ComputerNames[PlayerName];
+ PlayerIp = 0;
+ FactionId = int.Parse(playerEntry[2]);
+ Team = int.Parse(playerEntry[4]);
+ }
+ else
+ {
+ PlayerIp = uint.Parse(playerEntry[1], System.Globalization.NumberStyles.HexNumber);
+ FactionId = int.Parse(playerEntry[5]);
+ Team = int.Parse(playerEntry[7]);
+ }
+ }
+ }
+}
diff --git a/ReplayFile/Replay.cs b/ReplayFile/Replay.cs
new file mode 100644
index 0000000..b360ae3
--- /dev/null
+++ b/ReplayFile/Replay.cs
@@ -0,0 +1,346 @@
+using AnotherReplayReader.Utils;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace AnotherReplayReader.ReplayFile
+{
+ internal enum ReplayType
+ {
+ Skirmish,
+ Lan,
+ Online
+ }
+
+ internal sealed class Replay
+ {
+ public static readonly byte[] HeaderMagic = Encoding.ASCII.GetBytes("RA3 REPLAY HEADER");
+ public static readonly Dictionary TypeStrings = new()
+ {
+ { ReplayType.Skirmish, "遭遇战录像" },
+ { ReplayType.Lan, "局域网录像" },
+ { ReplayType.Online, "官网录像" },
+ };
+
+ private readonly byte _replaySaverIndex;
+ private readonly byte[]? _rawHeader;
+
+ public string Path { get; }
+ public DateTime Date { get; }
+ public string MapName { get; }
+ public string MapPath { get; }
+ public IReadOnlyList Players { get; }
+ public int NumberOfPlayingPlayers { get; }
+ public Mod Mod { get; }
+ public ReplayType Type { get; }
+ public bool HasCommentator { get; }
+
+ public long Size { get; }
+ public ReplayFooter? Footer { get; }
+ public IReadOnlyList? 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;
+ try
+ {
+ var query = from splitted in description.Split(';')
+ where !string.IsNullOrWhiteSpace(splitted)
+ select splitted.Split(new[] { '=' }, 2);
+ entries = query.ToDictionary(x => x[0], x => x[1]);
+ }
+ catch (Exception e)
+ {
+ throw new InvalidDataException($"Failed to parse string header of replay {Path}: \r\n{e}");
+ }
+
+ try
+ {
+ Players = entries["S"].Split(':')
+ .TakeWhile(x => !string.IsNullOrWhiteSpace(x) && x[0] != 'X')
+ .Select(x => new Player(x.Split(',')))
+ .ToList();
+ }
+ catch (Exception exception)
+ {
+ throw new InvalidDataException($"Failed to parse playerdata from string header of replay {Path}: \r\n{exception}");
+ }
+
+ MapPath = entries["M"].Substring(3);
+
+ HasCommentator = !entries["PC"].Equals("-1");
+
+ var lanFlag = int.Parse(entries["GT"]) == 0;
+ if (lanFlag)
+ {
+ if (Players.First().PlayerIp == 0)
+ {
+ Type = ReplayType.Skirmish;
+ }
+ else
+ {
+ Type = ReplayType.Lan;
+ }
+ }
+ else
+ {
+ Type = ReplayType.Online;
+ }
+
+ _replaySaverIndex = reader.ReadByte();
+
+ reader.ReadBytes(8); // 8 bit paddings
+ var fileNameLength = reader.ReadInt32();
+ reader.ReadBytes(fileNameLength * 2);
+ reader.ReadBytes(16);
+ var verMagicLength = reader.ReadInt32();
+ reader.ReadBytes(verMagicLength);
+ reader.ReadBytes(85);
+
+ if (reader.BaseStream.Position != headerSize)
+ {
+ Debug.Instance.DebugMessage += $"Warning: the stored header size {headerSize} isn't correct (acutally {reader.BaseStream.Position})\r\n";
+ return;
+ }
+
+ if (!parseBody)
+ {
+ // jump to footer directly
+ reader.BaseStream.Seek(-4, SeekOrigin.End);
+ try
+ {
+ Footer = new ReplayFooter(reader, ReplayFooterOption.SeekToFooter);
+ }
+ catch (Exception e)
+ {
+ Debug.Instance.DebugMessage += $"Failed to parse replay footer, replay might be corrupt: {e}\r\n";
+ Footer = null;
+ }
+
+ return;
+ }
+
+ var body = new List();
+ try
+ {
+ while (true)
+ {
+ var timeCode = reader.ReadUInt32();
+ if (timeCode == ReplayFooter.Terminator)
+ {
+ Footer = new ReplayFooter(reader, ReplayFooterOption.CurrentlyAtFooter);
+ break;
+ }
+
+ body.Add(new ReplayChunk(timeCode, reader));
+ }
+ }
+ catch (Exception e)
+ {
+ Debug.Instance.DebugMessage += $"Failed to parse replay body, replay might be corrupt: {e}\r\n";
+ }
+
+ byte[]? rawHeader = null;
+ try
+ {
+ // 重新读取原来的整个录像头
+ reader.BaseStream.Seek(0, SeekOrigin.Begin);
+ rawHeader = reader.ReadBytes(headerSize);
+ }
+ catch (Exception e)
+ {
+ Debug.Instance.DebugMessage += $"Warning: failed to read raw header: {e}\r\n";
+ return;
+ }
+ if (rawHeader.Length != headerSize)
+ {
+ Debug.Instance.DebugMessage += $"Warning: the stored header size {headerSize} isn't correct (raw header length = {rawHeader.Length})\r\n";
+ return;
+ }
+
+ _rawHeader = rawHeader;
+ Body = body;
+ }
+
+ public Dictionary GetCommandCounts()
+ {
+ if (Body is null)
+ {
+ throw new InvalidOperationException("Replay body must be parsed before retrieving command chunks");
+ }
+
+ var playerCommands = new Dictionary();
+
+ foreach (var chunk in Body)
+ {
+ if (chunk.Type != 1)
+ {
+ continue;
+ }
+
+ foreach (var command in CommandChunk.Parse(chunk))
+ {
+ var commandCount = playerCommands.TryGetValue(command.CommandId, out var current) ? current : new int[Players.Count];
+ if (command.PlayerIndex >= commandCount.Length) // unknown or unparsable command?
+ {
+ commandCount = commandCount
+ .Concat(new int[command.PlayerIndex - commandCount.Length + 1])
+ .ToArray();
+ }
+ commandCount[command.PlayerIndex] = commandCount[command.PlayerIndex] + 1;
+ playerCommands[command.CommandId] = commandCount;
+ }
+ }
+
+ return playerCommands;
+ }
+
+ public Replay CloneHeader()
+ {
+ using var stream = new MemoryStream();
+ using (var writer = new BinaryWriter(stream, Encoding.UTF8, true))
+ {
+ WriteTo(writer);
+ }
+ stream.Position = 0;
+ return new Replay(Path, stream);
+ }
+
+ public bool PathEquals(Replay replay) => PathEquals(replay.Path);
+ public bool PathEquals(string path) => Path.Equals(path, StringComparison.OrdinalIgnoreCase);
+
+ public string GetDetails(PlayerIdentity playerIdentity)
+ {
+ static string GetSizeString(double size)
+ {
+ if (size > 1024 * 1024)
+ {
+ return $"{Math.Round(size / (1024 * 1024), 2)}MB";
+ }
+ return $"{Math.Round(size / 1024)}KB";
+ }
+
+ string size = GetSizeString(Size);
+ string length = Length?.ToString() ?? "录像已损坏,请先修复录像";
+
+ var replaySaver = _replaySaverIndex < Players.Count
+ ? ReplaySaver.PlayerName
+ : "[无法获取保存录像的玩家]";
+
+ using var writer = new StringWriter();
+ writer.WriteLine("文件名:{0}", FileName);
+ writer.WriteLine("大小:{0}", size);
+ writer.WriteLine("地图:{0}", MapName);
+ writer.WriteLine("日期:{0}", Date);
+ writer.WriteLine("长度:{0}", length);
+ writer.WriteLine("录像类别:{0}", TypeString);
+ writer.WriteLine("这个文件是{0}保存的", replaySaver);
+ writer.WriteLine("玩家列表:");
+ foreach (var player in Players)
+ {
+ if (player == Players.Last() && player.PlayerName.Equals("post Commentator"))
+ {
+ break;
+ }
+ var factionName = ModData.GetFaction(Mod, player.FactionId).Name;
+ var realName = Type == ReplayType.Lan ? playerIdentity.FormatRealName(player.PlayerIp) : string.Empty;
+ writer.WriteLine($"{player.PlayerName + realName},{factionName}");
+ }
+
+ return writer.ToString();
+ }
+
+ public void WriteTo(BinaryWriter writer) => WriteTo(writer, false);
+
+ private void WriteTo(BinaryWriter writer, bool skipBody)
+ {
+ if ((_rawHeader is null || Body is null) && !skipBody)
+ {
+ throw new InvalidOperationException("Replay body must be parsed before writing replay");
+ }
+
+ writer.Write(_rawHeader);
+
+ var lastTimeCode = Footer?.FinalTimeCode;
+ if (Body is not null)
+ {
+ foreach (var chunk in Body)
+ {
+ lastTimeCode = chunk.TimeCode;
+ writer.Write(chunk);
+ }
+ }
+
+ if (Footer is not null)
+ {
+ writer.Write(Footer);
+ }
+ else if (lastTimeCode is uint lastTimeCodeValue)
+ {
+ writer.Write(new ReplayFooter(lastTimeCodeValue));
+ }
+ }
+ }
+
+
+}
diff --git a/ReplayFile/ReplayChunk.cs b/ReplayFile/ReplayChunk.cs
new file mode 100644
index 0000000..b4943f1
--- /dev/null
+++ b/ReplayFile/ReplayChunk.cs
@@ -0,0 +1,37 @@
+using System.IO;
+
+namespace AnotherReplayReader.ReplayFile
+{
+ internal sealed class ReplayChunk
+ {
+ private readonly byte[] _data;
+
+ public uint TimeCode { get; }
+ public byte Type { get; }
+
+ public ReplayChunk(uint timeCode, BinaryReader reader)
+ {
+ TimeCode = timeCode; // reader.ReadUInt32();
+ Type = reader.ReadByte();
+ var chunkSize = reader.ReadInt32();
+ _data = reader.ReadBytes(chunkSize);
+ if (reader.ReadInt32() != 0)
+ {
+ throw new InvalidDataException("Replay Chunk not ended with zero");
+ }
+ }
+
+ public BinaryReader GetReader() => new(new MemoryStream(_data, false));
+
+ public void WriteTo(BinaryWriter writer)
+ {
+ writer.Write(TimeCode);
+ writer.Write(Type);
+ writer.Write(_data.Length);
+ writer.Write(_data);
+ writer.Write(0);
+ }
+ }
+
+
+}
diff --git a/ReplayFile/ReplayExtensions.cs b/ReplayFile/ReplayExtensions.cs
new file mode 100644
index 0000000..38b0589
--- /dev/null
+++ b/ReplayFile/ReplayExtensions.cs
@@ -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();
+ var lastTwoBytes = Array.Empty();
+ 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);
+ }
+
+
+}
diff --git a/ReplayFile/ReplayFooter.cs b/ReplayFile/ReplayFooter.cs
new file mode 100644
index 0000000..1b3a637
--- /dev/null
+++ b/ReplayFile/ReplayFooter.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace AnotherReplayReader.ReplayFile
+{
+ internal enum ReplayFooterOption
+ {
+ SeekToFooter,
+ CurrentlyAtFooter,
+ }
+
+ internal sealed class ReplayFooter
+ {
+ public const uint Terminator = 0x7FFFFFFF;
+ public static readonly byte[] FooterString = Encoding.ASCII.GetBytes("RA3 REPLAY FOOTER");
+
+ private readonly byte[] _data;
+
+ public uint FinalTimeCode { get; }
+ public TimeSpan ReplayLength => TimeSpan.FromSeconds(FinalTimeCode / 15.0);
+
+ public ReplayFooter(BinaryReader reader, ReplayFooterOption option)
+ {
+ var currentPosition = reader.BaseStream.Position;
+ reader.BaseStream.Seek(-4, SeekOrigin.End);
+ var footerLength = reader.ReadInt32();
+
+ if (option == ReplayFooterOption.SeekToFooter)
+ {
+ currentPosition = reader.BaseStream.Length - footerLength;
+ }
+
+ reader.BaseStream.Seek(currentPosition, SeekOrigin.Begin);
+ var footer = reader.ReadBytes(footerLength);
+
+ if (footer.Length != footerLength || reader.BaseStream.Position != reader.BaseStream.Length)
+ {
+ throw new InvalidDataException("Invalid footer");
+ }
+
+ using var footerStream = new MemoryStream(footer);
+ using var footerReader = new BinaryReader(footerStream);
+ var footerString = footerReader.ReadBytes(17);
+ if (!footerString.SequenceEqual(FooterString))
+ {
+ throw new InvalidDataException("Invalid footer, no footer string");
+ }
+ FinalTimeCode = footerReader.ReadUInt32();
+ _data = footerReader.ReadBytes(footer.Length - 25);
+ if (footerReader.ReadInt32() != footerLength)
+ {
+ throw new InvalidDataException();
+ }
+ }
+
+ public ReplayFooter(uint finalTimeCode)
+ {
+ FinalTimeCode = finalTimeCode;
+ _data = new byte[] { 0x02, 0x1A, 0x00, 0x00, 0x00 };
+ }
+
+ List? TryGetKillDeathRatio()
+ {
+ if (_data.Length < 24)
+ {
+ return null;
+ }
+
+ var ratios = new List();
+ using var stream = new MemoryStream(_data, _data.Length - 24, 24);
+ using var reader = new BinaryReader(stream);
+ ratios.Add(reader.ReadSingle());
+ throw new NotImplementedException();
+ }
+
+ public void WriteTo(BinaryWriter writer)
+ {
+ writer.Write(Terminator);
+ writer.Write(FooterString);
+ writer.Write(FinalTimeCode);
+ writer.Write(_data);
+ writer.Write(FooterString.Length + _data.Length + 8);
+ }
+ }
+
+
+}
diff --git a/TechnologyAssembler.Core.dll b/TechnologyAssembler.Core.dll
new file mode 100644
index 0000000..2c15d9e
Binary files /dev/null and b/TechnologyAssembler.Core.dll differ
diff --git a/Utils/CancelManager.cs b/Utils/CancelManager.cs
new file mode 100644
index 0000000..af3f9c0
--- /dev/null
+++ b/Utils/CancelManager.cs
@@ -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;
+ }
+ }
+}
diff --git a/Utils/CancellableTaskExtensions.cs b/Utils/CancellableTaskExtensions.cs
new file mode 100644
index 0000000..832f8b0
--- /dev/null
+++ b/Utils/CancellableTaskExtensions.cs
@@ -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);
+ }
+ }
+}
diff --git a/Utils/ImmutableArrayExtensions.cs b/Utils/ImmutableArrayExtensions.cs
new file mode 100644
index 0000000..f7458ac
--- /dev/null
+++ b/Utils/ImmutableArrayExtensions.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Immutable;
+
+namespace AnotherReplayReader.Utils
+{
+ static class ImmutableArrayExtensions
+ {
+ public static int? FindIndex(this in ImmutableArray a, Predicate p)
+ {
+ for (var i = 0; i < a.Length; ++i)
+ {
+ if (p(a[i]))
+ {
+ return i;
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/Utils/Lock.cs b/Utils/Lock.cs
new file mode 100644
index 0000000..f17268d
--- /dev/null
+++ b/Utils/Lock.cs
@@ -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(object @lock, Func action)
+ {
+ using var locker = new Lock(@lock);
+ return action();
+ }
+ }
+}
diff --git a/Utils/PinyinExtensions.cs b/Utils/PinyinExtensions.cs
new file mode 100644
index 0000000..d901680
--- /dev/null
+++ b/Utils/PinyinExtensions.cs
@@ -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(" ", "");
+ }
+ }
+}
diff --git a/Utils/RegistryUtils.cs b/Utils/RegistryUtils.cs
new file mode 100644
index 0000000..8e58b84
--- /dev/null
+++ b/Utils/RegistryUtils.cs
@@ -0,0 +1,28 @@
+using Microsoft.Win32;
+using System;
+
+namespace AnotherReplayReader.Utils
+{
+ internal static class RegistryUtils
+ {
+ public static string? Retrieve(RegistryHive hive, string path, string value)
+ {
+ try
+ {
+ using var view32 = RegistryKey.OpenBaseKey(hive, RegistryView.Registry32);
+ using var ra3Key = view32.OpenSubKey(path, false);
+ return ra3Key?.GetValue(value) as string;
+ }
+ catch (Exception e)
+ {
+ Debug.Instance.DebugMessage += $"Failed to retrieve registy {hive}:{path}:{value}: {e}";
+ return null;
+ }
+ }
+
+ public static string? RetrieveInRa3(RegistryHive hive, string value)
+ {
+ return Retrieve(hive, @"Software\Electronic Arts\Electronic Arts\Red Alert 3", value);
+ }
+ }
+}
diff --git a/Utils/ReplayPinyinList.cs b/Utils/ReplayPinyinList.cs
new file mode 100644
index 0000000..d3a8e4d
--- /dev/null
+++ b/Utils/ReplayPinyinList.cs
@@ -0,0 +1,61 @@
+using AnotherReplayReader.ReplayFile;
+using System.Collections.Immutable;
+using System.Linq;
+
+namespace AnotherReplayReader.Utils
+{
+ internal class ReplayPinyinList
+ {
+ private readonly PlayerIdentity _playerIdentity;
+ public ImmutableArray Replays { get; } = ImmutableArray.Empty;
+ public ImmutableArray Pinyins { get; } = ImmutableArray.Empty;
+
+ public ReplayPinyinList(PlayerIdentity playerIdentity) :
+ this(ImmutableArray.Empty, playerIdentity)
+ {
+ }
+
+ public ReplayPinyinList(ImmutableArray replay, PlayerIdentity playerIdentity) :
+ this(replay,
+ replay.Select(replay => new ReplayPinyinData(replay, playerIdentity)).ToImmutableArray(),
+ playerIdentity)
+ {
+ }
+
+ private ReplayPinyinList(ImmutableArray replay,
+ ImmutableArray pinyins,
+ PlayerIdentity playerIdentity)
+ {
+ _playerIdentity = playerIdentity;
+ Replays = replay;
+ Pinyins = pinyins;
+ }
+
+ public ReplayPinyinList SetItem(int index, Replay replay)
+ {
+ return new(Replays.SetItem(index, replay),
+ Pinyins.SetItem(index, new(replay, _playerIdentity)),
+ _playerIdentity);
+ }
+ }
+
+ class ReplayPinyinData
+ {
+ public Replay Replay { get; }
+ public string? PinyinDetails { get; }
+ public string? PinyinMod { get; }
+
+ public ReplayPinyinData(Replay replay, PlayerIdentity playerIdentity)
+ {
+ Replay = replay;
+ PinyinDetails = replay.GetDetails(playerIdentity).ToPinyin();
+ PinyinMod = replay.Mod.ModName.ToPinyin();
+ }
+
+ public bool MatchPinyin(string? pinyin)
+ {
+ return PinyinDetails?.ContainsIgnoreCase(pinyin) is true
+ || PinyinMod?.ContainsIgnoreCase(pinyin) is true;
+ }
+ }
+}
diff --git a/Utils/ShortTimeSpan.cs b/Utils/ShortTimeSpan.cs
new file mode 100644
index 0000000..ff83a66
--- /dev/null
+++ b/Utils/ShortTimeSpan.cs
@@ -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, IComparable, 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;
+ }
+}
diff --git a/Utils/TaskQueue.cs b/Utils/TaskQueue.cs
new file mode 100644
index 0000000..eabd2c8
--- /dev/null
+++ b/Utils/TaskQueue.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Threading;
+
+namespace AnotherReplayReader.Utils
+{
+ internal class TaskQueue
+ {
+ private readonly object _lock = new();
+ private readonly Dispatcher _dispatcher;
+ private Task _current = Task.CompletedTask;
+
+ public TaskQueue(Dispatcher dispatcher)
+ {
+ _dispatcher = dispatcher;
+ }
+
+ public Task Enqueue(Func getTask, CancellationToken cancelToken)
+ {
+ using var locker = new Lock(_lock);
+ _current = _current.ContinueWith(async t =>
+ {
+ await _dispatcher.InvokeAsync(getTask, DispatcherPriority.Background, cancelToken);
+ }, cancelToken);
+ return _current.IgnoreCancel();
+ }
+
+ }
+}
diff --git a/Window1.xaml b/Window1.xaml
index 9a64fee..fdd9701 100644
--- a/Window1.xaml
+++ b/Window1.xaml
@@ -8,15 +8,15 @@
mc:Ignorable="d"
Title="Window1" Height="450" Width="800">
-
+
-
+
-
-
+
+
diff --git a/Window1.xaml.cs b/Window1.xaml.cs
index ed294bb..72f8963 100644
--- a/Window1.xaml.cs
+++ b/Window1.xaml.cs
@@ -1,14 +1,11 @@
-using NPinyin;
+using AnotherReplayReader.Utils;
using System;
-using System.Collections.Generic;
-using System.IO;
using System.Linq;
using System.Net;
-using System.Text;
using System.Threading.Tasks;
using System.Web;
-using System.Web.Script.Serialization;
using System.Windows;
+using static System.Text.Json.JsonSerializer;
namespace AnotherReplayReader
{
@@ -17,7 +14,7 @@ namespace AnotherReplayReader
///
internal partial class Window1 : Window
{
- private PlayerIdentity _identity;
+ private readonly PlayerIdentity _identity;
public Window1(PlayerIdentity identity)
{
@@ -28,115 +25,108 @@ namespace AnotherReplayReader
private async void Refresh()
{
- Dispatcher.Invoke(() => _setIPButton.IsEnabled = false);
+ _setIPButton.IsEnabled = false;
try
{
- Dispatcher.Invoke(() =>
- {
- _dataGrid.Items.Clear();
- _dataGrid.Items.Add(new IPAndPlayer { IP = 0, ID = "正在加载..." });
- });
-
+ _dataGrid.Items.Clear();
+ _dataGrid.Items.Add(new IpAndPlayer { Ip = 0, Id = "正在加载..." });
await _identity.Fetch();
-
- Display();
+ await Display();
}
- catch(Exception e)
+ catch (Exception e)
{
- Dispatcher.Invoke(() => MessageBox.Show(this, $"无法加载IP表:{e}"));
+ MessageBox.Show(this, $"无法加载IP表:{e}");
+ }
+ finally
+ {
+ _setIPButton.IsEnabled = true;
}
- Dispatcher.Invoke(() => _setIPButton.IsEnabled = true);
}
- private void Display(string filter = "", string nameFilter = "")
+ private async Task Display(string filter = "", string nameFilter = "")
{
- var pinyin = nameFilter.ToPinyin();
- var newList = _identity
- .AsSortedList()
- .Where(x =>
- {
- if (!x.IPString.StartsWith(filter))
- {
- return false;
- }
- if (x.PinyinID?.ContainsIgnoreCase(pinyin) is true)
- {
- return true;
- }
- return x.ID.ContainsIgnoreCase(nameFilter);
- })
- .ToArray();
- Dispatcher.Invoke(() =>
+ var newList = await Task.Run(() =>
{
- _dataGrid.Items.Clear();
- foreach (var item in newList)
- {
- _dataGrid.Items.Add(item);
- }
+ var pinyin = nameFilter.ToPinyin();
+ return _identity
+ .AsSortedList()
+ .Where(x =>
+ {
+ if (!x.IpString.Contains(filter))
+ {
+ return false;
+ }
+ if (x.PinyinId?.ContainsIgnoreCase(pinyin) is true)
+ {
+ return true;
+ }
+ return x.Id.ContainsIgnoreCase(nameFilter);
+ })
+ .ToArray();
});
+
+ _dataGrid.Items.Clear();
+ _dataGrid.ItemsSource = newList;
+ _dataGrid.Items.Refresh();
}
private async void OnClick(object sender, RoutedEventArgs e)
{
- Dispatcher.Invoke(() => _setIPButton.IsEnabled = false);
-
- await Task.Run(() =>
+ _setIPButton.IsEnabled = false;
+ try
{
- var ipText = Dispatcher.Invoke(() => _ipField.Text);
-
+ var ipText = _ipField.Text;
if (!IPAddress.TryParse(ipText, out var ip))
{
- Dispatcher.Invoke(() => MessageBox.Show(this, "IP格式不正确"));
+ MessageBox.Show(this, "IP 格式不正确");
return;
}
-
- var idText = Dispatcher.Invoke(() => _idField.Text);
-
+ var idText = _idField.Text;
if (string.IsNullOrWhiteSpace(idText))
{
- var result = Dispatcher.Invoke(() => MessageBox.Show(this, "你没填输入任何说明,是否确认继续?", "注意", MessageBoxButton.OKCancel));
- if(result != MessageBoxResult.OK)
+ var choice = MessageBox.Show(this, "没有输入任何关于该玩家的说明,是否继续?", "注意", MessageBoxButton.OKCancel);
+ if (choice != MessageBoxResult.OK)
{
return;
}
}
- try
+ var result = await UpdateIpTable(ip, idText);
+ if (!result)
{
- var bytes = ip.GetAddressBytes();
- var ipNum = (uint)bytes[0] * 256 * 256 * 256 + bytes[1] * 256 * 256 + bytes[2] * 256 + bytes[3];
- var text = HttpUtility.UrlEncode(idText);
-
- var key = HttpUtility.UrlEncode(Auth.GetKey());
- var request = WebRequest.Create($"https://lanyi.altervista.org/playertable/playertable.php?do=setIP&ip={ipNum}&id={text}&key={key}");
-
- using (var stream = request.GetResponse().GetResponseStream())
- using (var reader = new StreamReader(stream))
- {
- var response = reader.ReadToEnd();
- var serializer = new JavaScriptSerializer();
- var result = serializer.Deserialize(response);
- if(!result)
- {
- Dispatcher.Invoke(() => MessageBox.Show(this, "设置IP表失败"));
- }
- }
- }
- catch (Exception exception)
- {
- Dispatcher.Invoke(() => MessageBox.Show(this, $"设置IP表时发生错误。\r\n{exception}"));
+ MessageBox.Show(this, "设置 IP 表失败");
}
- });
-
- Refresh();
+ Refresh();
+ }
+ catch (Exception exception)
+ {
+ MessageBox.Show(this, $"设置 IP 表时发生错误。\r\n{exception}");
+ }
+ finally
+ {
+ _setIPButton.IsEnabled = true;
+ }
}
- private async void OnIPFieldChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
+ private static async Task UpdateIpTable(IPAddress ip, string idText)
+ {
+ var bytes = ip.GetAddressBytes();
+ var ipNum = (uint)bytes[0] * 256 * 256 * 256 + bytes[1] * 256 * 256 + bytes[2] * 256 + bytes[3];
+ var text = HttpUtility.UrlEncode(idText);
+
+ var key = HttpUtility.UrlEncode(Auth.GetKey());
+ var request = WebRequest.Create($"https://lanyi.altervista.org/playertable/playertable.php?do=setIP&ip={ipNum}&id={text}&key={key}");
+ using var response = await request.GetResponseAsync().ConfigureAwait(false);
+ using var stream = response.GetResponseStream();
+ return await DeserializeAsync(stream).ConfigureAwait(false);
+ }
+
+ private async void OnIpFieldChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
var ipText = _ipField.Text;
var idText = _idField.Text;
- await Task.Run(() => Display(ipText, idText));
+ await Display(ipText, idText);
}
private void DataGrid_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)