diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index e9fefed..911ee4e 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,9 +1,10 @@
name: .NET Build & Test
on:
- - push
- - pull_request
- - workflow_call
+ push:
+ pull_request:
+ workflow_call:
+ workflow_dispatch:
jobs:
build:
diff --git a/Directory.Build.props b/Directory.Build.props
index 4a95f88..2c2c7db 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -4,9 +4,6 @@
True
latest
-
-
-
Nodsoft Systems
imkindaprogrammermyself, Sakura Akeno Isayeki, floribe2000, StewieoO
WoWS Replays Unpack
@@ -33,10 +30,6 @@
-
-
-
-
diff --git a/Nodsoft.WowsReplaysUnpack.Console/Nodsoft.WowsReplaysUnpack.Console.csproj b/Nodsoft.WowsReplaysUnpack.Console/Nodsoft.WowsReplaysUnpack.Console.csproj
index ff27f56..bbc188a 100644
--- a/Nodsoft.WowsReplaysUnpack.Console/Nodsoft.WowsReplaysUnpack.Console.csproj
+++ b/Nodsoft.WowsReplaysUnpack.Console/Nodsoft.WowsReplaysUnpack.Console.csproj
@@ -16,6 +16,7 @@
+
diff --git a/Nodsoft.WowsReplaysUnpack.Console/Program.cs b/Nodsoft.WowsReplaysUnpack.Console/Program.cs
index 3d0bd32..b191b52 100644
--- a/Nodsoft.WowsReplaysUnpack.Console/Program.cs
+++ b/Nodsoft.WowsReplaysUnpack.Console/Program.cs
@@ -6,22 +6,31 @@
using Nodsoft.WowsReplaysUnpack.EntitySerializer;
using Nodsoft.WowsReplaysUnpack.ExtendedData;
using Nodsoft.WowsReplaysUnpack.ExtendedData.Models;
+using Nodsoft.WowsReplaysUnpack.FileStore.Definitions;
using Nodsoft.WowsReplaysUnpack.Services;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
+using System.Threading.Tasks;
-string samplePath = Path.Join(Directory.GetCurrentDirectory(), "../../../..", "Replay-Samples");
+string samplePath = Path.Join(Directory.GetCurrentDirectory(), "../../../../Nodsoft.WowsReplaysUnpack.Tests", "Replay-Samples");
FileStream _GetReplayFile(string name) => File.OpenRead(Path.Join(samplePath, name));
ServiceProvider? services = new ServiceCollection()
- .AddWowsReplayUnpacker(builder =>
- {
- //builder.AddReplayController();
- builder.AddExtendedData();
- })
+ //.AddWowsReplayUnpacker(builder =>
+ //{
+ // //builder.AddReplayController();
+ // //builder.AddExtendedData();
+ //})
+ .AddWowsReplayUnpacker(builder => builder
+ .WithDefinitionLoader())
+ .Configure(options =>
+ {
+ options.RootDirectory = options.RootDirectory = Path.Join(Directory.GetCurrentDirectory(),
+ "..", "..", "..", "..", "Nodsoft.WowsReplaysUnpack.Core", "Definitions", "Versions");
+ })
.AddLogging(logging =>
{
logging.ClearProviders();
@@ -41,8 +50,32 @@
// Console.WriteLine($"[{GetGroupString(msg)}] {msg.EntityId} : {msg.MessageContent}");
//}
+const int CYCLE = 20;
+async Task syncTasks(bool sync)
+{
+ List unpackedReplays = new List();
+ if (sync)
+ {
+ for (int i = 0; i < CYCLE; i++)
+ {
+ replayUnpacker.GetUnpacker().Unpack(_GetReplayFile("good.wowsreplay"));
+ }
+ }
+ else
+ {
+ Parallel.ForEach(Enumerable.Range(0, CYCLE), (i) =>
+ {
+ unpackedReplays.Add(replayUnpacker.GetUnpacker().Unpack(_GetReplayFile("good.wowsreplay")));
+ });
+ }
+ return unpackedReplays.ToArray();
+}
+DateTime start = DateTime.Now;
+await syncTasks(false);
+Console.WriteLine(DateTime.Now - start);
+var goodReplay = replayUnpacker.GetUnpacker().Unpack(_GetReplayFile("good.wowsreplay"));
var alphaReplay = replayUnpacker.GetUnpacker().Unpack(_GetReplayFile("press_account_alpha.wowsreplay"));
var bravoReplay = replayUnpacker.GetUnpacker().Unpack(_GetReplayFile("unfinished_replay.wowsreplay"));
diff --git a/Nodsoft.WowsReplaysUnpack.Core/Consts.cs b/Nodsoft.WowsReplaysUnpack.Core/Consts.cs
index 4ecbe1c..9a3c669 100644
--- a/Nodsoft.WowsReplaysUnpack.Core/Consts.cs
+++ b/Nodsoft.WowsReplaysUnpack.Core/Consts.cs
@@ -3,4 +3,8 @@
public static class Consts
{
public const int Infinity = 0xFFFF;
+
+ public const char Underscore = '_';
+ public const char Dot = '.';
+ public const char Comma = ',';
}
\ No newline at end of file
diff --git a/Nodsoft.WowsReplaysUnpack.Core/Definitions/AssemblyDefinitionLoader.cs b/Nodsoft.WowsReplaysUnpack.Core/Definitions/AssemblyDefinitionLoader.cs
new file mode 100644
index 0000000..4862f10
--- /dev/null
+++ b/Nodsoft.WowsReplaysUnpack.Core/Definitions/AssemblyDefinitionLoader.cs
@@ -0,0 +1,114 @@
+using Nodsoft.WowsReplaysUnpack.Core.Extensions;
+using System.Reflection;
+using System.Xml;
+
+namespace Nodsoft.WowsReplaysUnpack.Core.Definitions;
+public class AssemblyDefinitionLoader : IDefinitionLoader
+{
+ private static readonly XmlReaderSettings _xmlReaderSettings = new() { IgnoreComments = true };
+
+ ///
+ /// Assembly of the Definition store (defaults to the implementation assembly).
+ ///
+ protected readonly Assembly Assembly;
+
+ ///
+ /// Version -> Definitions Directory
+ ///
+ protected readonly Dictionary DirectoryCache = new();
+
+ public AssemblyDefinitionLoader()
+ {
+
+ Assembly = typeof(DefaultDefinitionStore).Assembly;
+ }
+
+ ///
+ public Version[] GetSupportedVersions()
+ {
+ string versionsDirectory = JoinPath(Assembly.FullName!.GetStringBeforeIndex(','), "Definitions", "Versions");
+ return Assembly.GetManifestResourceNames()
+ .Where(name => name.StartsWith(versionsDirectory))
+ .Select(name => name.GetStringAfterLength(versionsDirectory + '.').GetStringBeforeIndex('.')[1..])
+ .Distinct()
+ .Select(static version => version.Split('_').Select(int.Parse).ToArray())
+ .Select(static arr => new Version(arr[0], arr[1], arr[2]))
+ .OrderByDescending(static version => version)
+ .ToArray();
+ }
+
+ ///
+ public virtual XmlDocument GetFileAsXml(Version clientVersion, string name, params string[] directoryNames)
+ {
+ DefinitionDirectory directory = FindDirectory(clientVersion, directoryNames);
+
+ if (directory.Files.SingleOrDefault(f => f.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) is not { } file)
+ {
+ throw new InvalidOperationException("File could not be found");
+ }
+
+ XmlReader reader = XmlReader.Create(Assembly.GetManifestResourceStream(file.Path) ?? throw new InvalidOperationException("File not found"), _xmlReaderSettings);
+ XmlDocument xmlDocument = new();
+ xmlDocument.Load(reader);
+
+ return xmlDocument;
+ }
+
+ ///
+ /// Finds a definition directory by given names.
+ ///
+ /// The game client version.
+ /// The names of the directories.
+ /// The definition directory.
+ public virtual DefinitionDirectory FindDirectory(Version clientVersion, IEnumerable directoryNames)
+ {
+ DefinitionDirectory folder = GetRootDirectory(clientVersion);
+
+ foreach (string? folderName in directoryNames)
+ {
+ DefinitionDirectory? foundFolder = folder.Directories.SingleOrDefault(f => f.Name.Equals(folderName, StringComparison.InvariantCultureIgnoreCase));
+
+ if (foundFolder is null)
+ {
+ break;
+ }
+
+ folder = foundFolder;
+ }
+
+ return folder;
+ }
+
+ ///
+ /// Gets the root definition directory for a given game client version.
+ ///
+ /// The game client version.
+ /// The root definition directory.
+ public virtual DefinitionDirectory GetRootDirectory(Version clientVersion)
+ {
+ if (DirectoryCache.TryGetValue(clientVersion.ToString(), out DefinitionDirectory? rootDirectory))
+ {
+ return rootDirectory;
+ }
+
+ string scriptsDirectory = JoinPath(Assembly.FullName!.GetStringBeforeIndex(Consts.Comma),
+ "Definitions", "Versions", $"{Consts.Underscore}{clientVersion.ToString().Replace(Consts.Dot, Consts.Underscore)}", "scripts"
+ );
+
+ string[] fileNames = Assembly.GetManifestResourceNames()
+ .Where(name => name.StartsWith(scriptsDirectory))
+ .ToArray();
+
+ rootDirectory = new("scripts", scriptsDirectory, fileNames);
+ DirectoryCache.Add(clientVersion.ToString(), rootDirectory);
+
+ return rootDirectory;
+ }
+
+ ///
+ /// Joins parts of an XML path, separated by a dot.
+ ///
+ /// Parts of the path.
+ /// The joined path.
+ protected static string JoinPath(params string[] parts) => string.Join('.', parts);
+}
diff --git a/Nodsoft.WowsReplaysUnpack.Core/Definitions/DefaultDefinitionStore.cs b/Nodsoft.WowsReplaysUnpack.Core/Definitions/DefaultDefinitionStore.cs
index 6acb473..0971d43 100644
--- a/Nodsoft.WowsReplaysUnpack.Core/Definitions/DefaultDefinitionStore.cs
+++ b/Nodsoft.WowsReplaysUnpack.Core/Definitions/DefaultDefinitionStore.cs
@@ -2,7 +2,6 @@
using Nodsoft.WowsReplaysUnpack.Core.DataTypes;
using Nodsoft.WowsReplaysUnpack.Core.Exceptions;
using Nodsoft.WowsReplaysUnpack.Core.Extensions;
-using System.Reflection;
using System.Xml;
namespace Nodsoft.WowsReplaysUnpack.Core.Definitions;
@@ -22,18 +21,9 @@ public class DefaultDefinitionStore : IDefinitionStore
///
/// Logger instance used by this definition store.
///
- protected readonly ILogger Logger;
+ protected ILogger Logger { get; }
- ///
- /// Assembly of the Definition store (defaults to the implementation assembly).
- ///
- protected readonly Assembly Assembly;
-
-
- ///
- /// Version -> Definitions Directory
- ///
- protected readonly Dictionary DirectoryCache = new();
+ protected IDefinitionLoader DefinitionLoader { get; }
///
/// version_name -> Definition
@@ -55,25 +45,13 @@ public class DefaultDefinitionStore : IDefinitionStore
///
protected readonly Dictionary> TypeMappings = new();
- public DefaultDefinitionStore(ILogger logger)
+ public DefaultDefinitionStore(ILogger logger, IDefinitionLoader definitionLoader)
{
- Assembly = typeof(DefaultDefinitionStore).Assembly;
- Logger = logger;
- string versionsDirectory = JoinPath(Assembly.FullName![..Assembly.FullName!.IndexOf(',')], "Definitions", "Versions");
+ Logger = logger;
+ DefinitionLoader = definitionLoader;
- _supportedVersions = Assembly.GetManifestResourceNames()
- .Where(name => name.StartsWith(versionsDirectory))
- .Select(name =>
- {
- string tempQualifier = name[$"{versionsDirectory}.".Length..];
- return tempQualifier[..tempQualifier.IndexOf('.')][1..];
- })
- .Distinct()
- .Select(version => version.Split('_').Select(int.Parse).ToArray())
- .Select(arr => new Version(arr[0], arr[1], arr[2]))
- .OrderByDescending(version => version)
- .ToArray();
+ _supportedVersions = DefinitionLoader.GetSupportedVersions();
}
#region EntityDefinitions
@@ -131,31 +109,15 @@ protected virtual string GetEntityDefinitionName(Version clientVersion, int inde
/// A dictionary of entity indexes and their names.
protected virtual Dictionary GetEntityIndexes(Version clientVersion)
{
- return GetFileAsXml(clientVersion, EntitiesXml).DocumentElement!.SelectSingleNode("ClientServerEntities")!.ChildNodes()
+ return DefinitionLoader.GetFileAsXml(GetActualVersion(clientVersion), EntitiesXml).DocumentElement!.SelectSingleNode("ClientServerEntities")!.ChildNodes()
.Select((node, index) => new { node, index })
.ToDictionary(i => i.index, i => i.node.Name);
}
#endregion
- ///
- public virtual XmlDocument GetFileAsXml(Version clientVersion, string name, params string[] directoryNames)
- {
- clientVersion = GetActualVersion(clientVersion);
- DefinitionDirectory directory = FindDirectory(clientVersion, directoryNames);
-
- if (directory.Files.SingleOrDefault(f => f.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) is not { } file)
- {
- throw new InvalidOperationException("File could not be found");
- }
-
- XmlReaderSettings settings = new() { IgnoreComments = true };
- XmlReader reader = XmlReader.Create(Assembly.GetManifestResourceStream(file.Path) ?? throw new InvalidOperationException("File not found"), settings);
- XmlDocument xmlDocument = new();
- xmlDocument.Load(reader);
-
- return xmlDocument;
- }
+ public virtual XmlDocument GetFileAsXml(Version clientVersion, string name, params string[] directoryNames)
+ => DefinitionLoader.GetFileAsXml(GetActualVersion(clientVersion), name, directoryNames);
///
public virtual DataTypeBase GetDataType(Version clientVersion, XmlNode typeOrArgXmlNode)
@@ -168,7 +130,7 @@ public virtual DataTypeBase GetDataType(Version clientVersion, XmlNode typeOrArg
return GetDataTypeInternal(clientVersion, typeMapping, typeOrArgXmlNode);
}
- XmlDocument aliasXml = GetFileAsXml(clientVersion, "alias.xml", "entity_defs");
+ XmlDocument aliasXml = DefinitionLoader.GetFileAsXml(clientVersion, "alias.xml", "entity_defs");
typeMapping = new();
foreach (XmlNode node in aliasXml.DocumentElement!.ChildNodes)
@@ -181,56 +143,6 @@ public virtual DataTypeBase GetDataType(Version clientVersion, XmlNode typeOrArg
return GetDataTypeInternal(clientVersion, typeMapping, typeOrArgXmlNode);
}
- ///
- /// Finds a definition directory by given names.
- ///
- /// The game client version.
- /// The names of the directories.
- /// The definition directory.
- protected virtual DefinitionDirectory FindDirectory(Version clientVersion, IEnumerable directoryNames)
- {
- DefinitionDirectory folder = GetRootDirectory(clientVersion);
-
- foreach (string? folderName in directoryNames)
- {
- DefinitionDirectory? foundFolder = folder.Directories.SingleOrDefault(f => f.Name.Equals(folderName, StringComparison.InvariantCultureIgnoreCase));
-
- if (foundFolder is null)
- {
- break;
- }
-
- folder = foundFolder;
- }
-
- return folder;
- }
-
- ///
- /// Gets the root definition directory for a given game client version.
- ///
- /// The game client version.
- /// The root definition directory.
- protected virtual DefinitionDirectory GetRootDirectory(Version clientVersion)
- {
- if (DirectoryCache.TryGetValue(clientVersion.ToString(), out DefinitionDirectory? rootDirectory))
- {
- return rootDirectory;
- }
-
- string scriptsDirectory = JoinPath(Assembly.FullName![..Assembly.FullName!.IndexOf(',')],
- "Definitions", "Versions", $"_{clientVersion.ToString().Replace('.', '_')}", "scripts"
- );
-
- string[] fileNames = Assembly.GetManifestResourceNames()
- .Where(name => name.StartsWith(scriptsDirectory))
- .ToArray();
-
- rootDirectory = new("scripts", scriptsDirectory, fileNames);
- DirectoryCache.Add(clientVersion.ToString(), rootDirectory);
-
- return rootDirectory;
- }
///
/// Internal method for getting a data type.
@@ -273,13 +185,6 @@ private Version GetActualVersion(Version version)
return actualVersion;
}
- ///
- /// Joins parts of an XML path, separated by a dot.
- ///
- /// Parts of the path.
- /// The joined path.
- protected static string JoinPath(params string[] parts) => string.Join('.', parts);
-
///
/// Joins parts of a cache key, separated by an underscore.
///
diff --git a/Nodsoft.WowsReplaysUnpack.Core/Definitions/DefinitionDirectory.cs b/Nodsoft.WowsReplaysUnpack.Core/Definitions/DefinitionDirectory.cs
index 504250f..0096f67 100644
--- a/Nodsoft.WowsReplaysUnpack.Core/Definitions/DefinitionDirectory.cs
+++ b/Nodsoft.WowsReplaysUnpack.Core/Definitions/DefinitionDirectory.cs
@@ -40,10 +40,9 @@ private void ParseChildrenRecursive(string[] fileNames)
foreach (string fileName in fileNames)
{
- string after = $"{Path}.";
- string actualFileName = fileName[after.Length..];
+ string actualFileName = fileName.GetStringAfterLength(Path + '.');
- if (actualFileName.Count(c => c is '.') is 1)
+ if (actualFileName.Count(static c => c is '.') is 1)
{
Files.Add(new(actualFileName, fileName));
}
diff --git a/Nodsoft.WowsReplaysUnpack.Core/Definitions/EntityDefinition.cs b/Nodsoft.WowsReplaysUnpack.Core/Definitions/EntityDefinition.cs
index 55e949f..f286c97 100644
--- a/Nodsoft.WowsReplaysUnpack.Core/Definitions/EntityDefinition.cs
+++ b/Nodsoft.WowsReplaysUnpack.Core/Definitions/EntityDefinition.cs
@@ -18,10 +18,8 @@ public record EntityDefinition : BaseDefinition
///
public List ClientMethods { get; private set; } = new();
- public EntityDefinition(Version clientVersion, IDefinitionStore definitionStore,
- string name) : base(clientVersion, definitionStore, name, ENTITY_DEFS)
- {
- }
+ public EntityDefinition(Version clientVersion, IDefinitionStore definitionStore, string name)
+ : base(clientVersion, definitionStore, name, ENTITY_DEFS) { }
///
/// Parses a .def file for the entity definition.
diff --git a/Nodsoft.WowsReplaysUnpack.Core/Definitions/IDefinitionLoader.cs b/Nodsoft.WowsReplaysUnpack.Core/Definitions/IDefinitionLoader.cs
new file mode 100644
index 0000000..f9206f2
--- /dev/null
+++ b/Nodsoft.WowsReplaysUnpack.Core/Definitions/IDefinitionLoader.cs
@@ -0,0 +1,21 @@
+using System.Xml;
+
+namespace Nodsoft.WowsReplaysUnpack.Core.Definitions;
+
+public interface IDefinitionLoader
+{
+ ///
+ /// Reads XML data from a .def file (seeking through directories as needed).
+ ///
+ /// Game client version
+ /// Name of the .def file to read
+ /// Directories where to search for the .def file
+ /// XML data
+ XmlDocument GetFileAsXml(Version clientVersion, string name, params string[] directoryNames);
+
+ ///
+ /// Gets all game client versions supported by this definition loader.
+ ///
+ /// An array of all supported game client versions ordered newest -> oldest.
+ Version[] GetSupportedVersions();
+}
\ No newline at end of file
diff --git a/Nodsoft.WowsReplaysUnpack.Core/Definitions/IDefinitionStore.cs b/Nodsoft.WowsReplaysUnpack.Core/Definitions/IDefinitionStore.cs
index f098e9c..5f06955 100644
--- a/Nodsoft.WowsReplaysUnpack.Core/Definitions/IDefinitionStore.cs
+++ b/Nodsoft.WowsReplaysUnpack.Core/Definitions/IDefinitionStore.cs
@@ -16,7 +16,7 @@ public interface IDefinitionStore
/// XML node to get the data type of
/// Data type of the XML node
DataTypeBase GetDataType(Version clientVersion, XmlNode typeOrArgXmlNode);
-
+
///
/// Gets an entity definition by its index.
///
@@ -24,7 +24,7 @@ public interface IDefinitionStore
/// Index of the entity definition
/// Entity definition
EntityDefinition GetEntityDefinition(Version clientVersion, int index);
-
+
///
/// Gets a property definition by its name.
///
@@ -32,9 +32,9 @@ public interface IDefinitionStore
/// Name of the property definition
/// Property definition
EntityDefinition GetEntityDefinition(Version clientVersion, string name);
-
+
///
- /// Reads XML data from a .def file (seeking through directories as needed).
+ /// Gets the definition file from the definition loader
///
/// Game client version
/// Name of the .def file to read
diff --git a/Nodsoft.WowsReplaysUnpack.Core/Extensions/StringExtensions.cs b/Nodsoft.WowsReplaysUnpack.Core/Extensions/StringExtensions.cs
index 79efe1c..79a1391 100644
--- a/Nodsoft.WowsReplaysUnpack.Core/Extensions/StringExtensions.cs
+++ b/Nodsoft.WowsReplaysUnpack.Core/Extensions/StringExtensions.cs
@@ -1,4 +1,11 @@
namespace Nodsoft.WowsReplaysUnpack.Core.Extensions;
public static class StringExtensions
-{ }
\ No newline at end of file
+{
+ public static string GetStringBeforeIndex(this string str, char before) => str[..str.IndexOf(before)];
+
+ public static string GetStringBeforeIndex(this string str, string before) => str[..str.IndexOf(before, StringComparison.Ordinal)];
+
+ public static string GetStringAfterLength(this string str, string after) => str[after.Length..];
+}
+
\ No newline at end of file
diff --git a/Nodsoft.WowsReplaysUnpack.Core/Nodsoft.WowsReplaysUnpack.Core.csproj b/Nodsoft.WowsReplaysUnpack.Core/Nodsoft.WowsReplaysUnpack.Core.csproj
index a38cb30..e61cd0a 100644
--- a/Nodsoft.WowsReplaysUnpack.Core/Nodsoft.WowsReplaysUnpack.Core.csproj
+++ b/Nodsoft.WowsReplaysUnpack.Core/Nodsoft.WowsReplaysUnpack.Core.csproj
@@ -1,4 +1,4 @@
-
+
Library
diff --git a/Nodsoft.WowsReplaysUnpack.FileStore/Definitions/FileSystemDefinitionLoader.cs b/Nodsoft.WowsReplaysUnpack.FileStore/Definitions/FileSystemDefinitionLoader.cs
new file mode 100644
index 0000000..cb2e4a6
--- /dev/null
+++ b/Nodsoft.WowsReplaysUnpack.FileStore/Definitions/FileSystemDefinitionLoader.cs
@@ -0,0 +1,69 @@
+using Microsoft.Extensions.Options;
+using Nodsoft.WowsReplaysUnpack.Core.Definitions;
+using System.Runtime.CompilerServices;
+using System.Xml;
+
+namespace Nodsoft.WowsReplaysUnpack.FileStore.Definitions;
+
+///
+/// Provides a definition loader using the filesystem as a backing store.
+///
+public class FileSystemDefinitionLoader : IDefinitionLoader
+{
+ private static readonly XmlReaderSettings _xmlReaderSettings = new()
+ {
+ IgnoreComments = true
+ };
+
+ private readonly IOptionsMonitor _options;
+
+ public FileSystemDefinitionLoader(IOptionsMonitor options)
+ {
+ _options = options;
+ }
+
+ ///
+ public XmlDocument GetFileAsXml(Version clientVersion, string name, params string[] directoryNames)
+ {
+ string path = Path.Combine(_options.CurrentValue.RootDirectory, ToFilesystemString(clientVersion), "scripts", Path.Combine(directoryNames), name);
+
+ if (!File.Exists(path))
+ {
+ throw new FileNotFoundException($"Could not find definition file {name} in {path}");
+ }
+
+ using Stream stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read);
+
+ // Use an XmlReader to load the file, as it is more efficient than loading the entire file into memory.
+ using XmlReader reader = XmlReader.Create(stream, _xmlReaderSettings);
+ XmlDocument document = new();
+ document.Load(reader);
+ return document;
+ }
+
+ ///
+ public Version[] GetSupportedVersions()
+ {
+ // For a set directory, we're expecting the child directories to be the versions.
+ // These will be structured by __ (e.g. 0_11_0),
+ // so we'll need to parse them accordingly into Version objects.
+ Version[] versions = new DirectoryInfo(_options.CurrentValue.RootDirectory).GetDirectories()
+ .Select(static dir => FromFilesystemString(dir.Name))
+ .OrderByDescending(static version => version)
+ .ToArray();
+
+ return versions;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static string ToFilesystemString(Version version)
+ {
+ return version.ToString().Replace('.', '_');
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Version FromFilesystemString(string version)
+ {
+ return new(version.Replace('_', '.'));
+ }
+}
\ No newline at end of file
diff --git a/Nodsoft.WowsReplaysUnpack.FileStore/Definitions/FileSystemDefinitionLoaderOptions.cs b/Nodsoft.WowsReplaysUnpack.FileStore/Definitions/FileSystemDefinitionLoaderOptions.cs
new file mode 100644
index 0000000..1319332
--- /dev/null
+++ b/Nodsoft.WowsReplaysUnpack.FileStore/Definitions/FileSystemDefinitionLoaderOptions.cs
@@ -0,0 +1,17 @@
+namespace Nodsoft.WowsReplaysUnpack.FileStore.Definitions;
+
+///
+/// Defines settings for the .
+///
+public record FileSystemDefinitionLoaderOptions
+{
+ ///
+ /// Gets or sets the path to the directory containing the definitions.
+ ///
+ public string RootDirectory { get; set; }
+
+ ///
+ /// Gets or sets whether the loader should poll the filesystem for changes.
+ ///
+ public bool EnableChangePolling { get; set; }
+}
\ No newline at end of file
diff --git a/Nodsoft.WowsReplaysUnpack.FileStore/Nodsoft.WowsReplaysUnpack.FileStore.csproj b/Nodsoft.WowsReplaysUnpack.FileStore/Nodsoft.WowsReplaysUnpack.FileStore.csproj
new file mode 100644
index 0000000..692e682
--- /dev/null
+++ b/Nodsoft.WowsReplaysUnpack.FileStore/Nodsoft.WowsReplaysUnpack.FileStore.csproj
@@ -0,0 +1,18 @@
+
+
+
+ Library
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Nodsoft.WowsReplaysUnpack.Tests/Nodsoft.WowsReplaysUnpack.Tests.csproj b/Nodsoft.WowsReplaysUnpack.Tests/Nodsoft.WowsReplaysUnpack.Tests.csproj
index 1461a8a..8b5bb33 100644
--- a/Nodsoft.WowsReplaysUnpack.Tests/Nodsoft.WowsReplaysUnpack.Tests.csproj
+++ b/Nodsoft.WowsReplaysUnpack.Tests/Nodsoft.WowsReplaysUnpack.Tests.csproj
@@ -24,6 +24,7 @@
+
diff --git a/Nodsoft.WowsReplaysUnpack.Tests/ReplayParsingExecutionTests.cs b/Nodsoft.WowsReplaysUnpack.Tests/ReplayParsingExecutionTests.cs
new file mode 100644
index 0000000..0cda896
--- /dev/null
+++ b/Nodsoft.WowsReplaysUnpack.Tests/ReplayParsingExecutionTests.cs
@@ -0,0 +1,67 @@
+using Microsoft.Extensions.DependencyInjection;
+using Nodsoft.WowsReplaysUnpack.Core.Models;
+using Nodsoft.WowsReplaysUnpack.FileStore.Definitions;
+using Nodsoft.WowsReplaysUnpack.Services;
+using Xunit;
+
+
+namespace Nodsoft.WowsReplaysUnpack.Tests;
+
+///
+/// Provides integration tests for the different replay parser configurations.
+///
+public class ReplayParsingExecutionTests
+{
+ ///
+ /// Tests the parsing of a replay file, using the default configuration.
+ ///
+ [Fact]
+ public void TestDefaultParsing()
+ {
+ // Arrange
+ IServiceProvider services = new ServiceCollection()
+ .AddLogging()
+ .AddWowsReplayUnpacker()
+ .BuildServiceProvider();
+
+ ReplayUnpackerFactory unpackerFactory = services.GetRequiredService();
+ IReplayUnpackerService service = unpackerFactory.GetUnpacker();
+
+ // Act
+ UnpackedReplay metadata = service.Unpack(Utilities.LoadReplay("good.wowsreplay"));
+
+ // Assert
+ Assert.NotNull(metadata);
+ }
+
+ ///
+ /// Tests the parsing of a replay file, using the filesystem defs loader.
+ ///
+ ///
+ /// This test requires the filesystem to be setup correctly, and definitions to be present.
+ ///
+ [Fact]
+ public void TestFilesystemParsing()
+ {
+ // Arrange
+ IServiceProvider services = new ServiceCollection()
+ .AddLogging()
+ .AddWowsReplayUnpacker(builder => builder
+ .WithDefinitionLoader())
+ .Configure(options =>
+ {
+ options.RootDirectory = options.RootDirectory = Path.Join(Directory.GetCurrentDirectory(),
+ "..", "..", "..", "..", "Nodsoft.WowsReplaysUnpack.Core", "Definitions", "Versions");
+ })
+ .BuildServiceProvider();
+
+ ReplayUnpackerFactory unpackerFactory = services.GetRequiredService();
+ IReplayUnpackerService service = unpackerFactory.GetUnpacker();
+
+ // Act
+ UnpackedReplay metadata = service.Unpack(Utilities.LoadReplay("good.wowsreplay"));
+
+ // Assert
+ Assert.NotNull(metadata);
+ }
+}
\ No newline at end of file
diff --git a/Nodsoft.WowsReplaysUnpack.Tests/ReplaySanitizerTests.cs b/Nodsoft.WowsReplaysUnpack.Tests/ReplaySanitizerTests.cs
index ca41ead..f77e69d 100644
--- a/Nodsoft.WowsReplaysUnpack.Tests/ReplaySanitizerTests.cs
+++ b/Nodsoft.WowsReplaysUnpack.Tests/ReplaySanitizerTests.cs
@@ -7,10 +7,13 @@
namespace Nodsoft.WowsReplaysUnpack.Tests;
+///
+/// Provides tests for the replay RCE detection system.
+///
public class ReplaySanitizerTests
{
private readonly ReplayUnpackerFactory _factory;
- private readonly string _sampleFolder = Path.Join(Directory.GetCurrentDirectory(), "Replay-Samples");
+
public ReplaySanitizerTests()
{
@@ -27,26 +30,16 @@ public ReplaySanitizerTests()
[Fact]
public void TestGoodReplay_Pass()
{
- UnpackedReplay replay = _factory.GetUnpacker().Unpack(LoadReplay("good.wowsreplay"));
+ UnpackedReplay replay = _factory.GetUnpacker().Unpack(Utilities.LoadReplay("good.wowsreplay"));
Assert.NotNull(replay);
}
- private MemoryStream LoadReplay(string replayPath)
- {
- using FileStream fs = File.OpenRead(Path.Join(_sampleFolder, replayPath));
- MemoryStream ms = new();
- fs.CopyTo(ms);
- ms.Position = 0;
-
- return ms;
- }
-
///
/// Test malicious replay detection (Assets/payload.wowsreplay)
///
[Fact]
public void TestPayloadReplayDetection()
{
- Assert.Throws(() => _factory.GetUnpacker().Unpack(LoadReplay("payload.wowsreplay")));
+ Assert.Throws(() => _factory.GetUnpacker().Unpack(Utilities.LoadReplay("payload.wowsreplay")));
}
}
\ No newline at end of file
diff --git a/Nodsoft.WowsReplaysUnpack.Tests/Utilities.cs b/Nodsoft.WowsReplaysUnpack.Tests/Utilities.cs
new file mode 100644
index 0000000..6e58243
--- /dev/null
+++ b/Nodsoft.WowsReplaysUnpack.Tests/Utilities.cs
@@ -0,0 +1,23 @@
+using System.Runtime.CompilerServices;
+
+namespace Nodsoft.WowsReplaysUnpack.Tests;
+
+///
+/// Helper methods used in tests.
+///
+public static class Utilities
+{
+ private static readonly string _sampleFolder = Path.Join(Directory.GetCurrentDirectory(), "Replay-Samples");
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static MemoryStream LoadReplay(string replayPath)
+ {
+ using FileStream fs = File.Open(Path.Join(_sampleFolder, replayPath), FileMode.Open, FileAccess.Read, FileShare.Read);
+ MemoryStream ms = new();
+ fs.CopyTo(ms);
+ fs.Close();
+ ms.Position = 0;
+
+ return ms;
+ }
+}
\ No newline at end of file
diff --git a/Nodsoft.WowsReplaysUnpack/ReplayUnpackerBuilder.cs b/Nodsoft.WowsReplaysUnpack/ReplayUnpackerBuilder.cs
new file mode 100644
index 0000000..5d761f7
--- /dev/null
+++ b/Nodsoft.WowsReplaysUnpack/ReplayUnpackerBuilder.cs
@@ -0,0 +1,113 @@
+using Microsoft.Extensions.DependencyInjection;
+using Nodsoft.WowsReplaysUnpack.Controllers;
+using Nodsoft.WowsReplaysUnpack.Core.Definitions;
+using Nodsoft.WowsReplaysUnpack.Services;
+
+namespace Nodsoft.WowsReplaysUnpack;
+
+///
+/// Provides a fluent API to build a WOWS replay data unpacker.
+///
+public class ReplayUnpackerBuilder
+{
+ private bool replayDataParserAdded;
+ private bool definitionStoreAdded;
+ private bool definitionLoaderAdded;
+
+ public IServiceCollection Services { get; }
+
+ ///
+ /// Intializes a new instance of the class,
+ /// by registering a as baseline.
+ ///
+ ///
+ public ReplayUnpackerBuilder(IServiceCollection services)
+ {
+ Services = services;
+ AddReplayController();
+ }
+
+ ///
+ /// Registers a for use in the WOWS replay data unpacker.
+ ///
+ /// The type of the replay data parser.
+ /// The builder.
+ public ReplayUnpackerBuilder WithReplayDataParser() where TParser : class, IReplayDataParser
+ {
+ Services.AddScoped();
+ replayDataParserAdded = true;
+ return this;
+ }
+
+ ///
+ /// Registers a for use in the WOWS replay data unpacker.
+ ///
+ /// The type of the replay controller.
+ /// The builder.
+ public ReplayUnpackerBuilder AddReplayController() where TController : class, IReplayController
+ {
+ Services.AddScoped>();
+ Services.AddScoped();
+ return this;
+ }
+
+ ///
+ /// Registers a for use in the WOWS replay data unpacker.
+ ///
+ /// The type of the definition loader.
+ /// The builder.
+ public ReplayUnpackerBuilder WithDefinitionLoader() where TLoader : class, IDefinitionLoader
+ {
+ Services.AddScoped();
+ definitionLoaderAdded = true;
+ return this;
+ }
+
+ ///
+ /// Registers a for use in the WOWS replay data unpacker.
+ ///
+ /// The type of the definition store.
+ /// The builder.
+ public ReplayUnpackerBuilder WithDefinitionStore() where TStore : class, IDefinitionStore
+ {
+ Services.AddSingleton();
+ definitionStoreAdded = true;
+ return this;
+ }
+
+
+ // stewie says: No need for that since they will be added either way if you don't add other ones
+ /////
+ ///// Registers the Assembly definition loader and the default definition store for the WOWS replay data unpacker.
+ ///// These are considered the default definition services for the unpacker.
+ /////
+ ///// The replay unpacker builder.
+ ///// The service collection.
+ //public static ReplayUnpackerBuilder WithDefaultDefinitions(this ReplayUnpackerBuilder builder)
+ //{
+ // builder.WithDefinitionLoader();
+ // builder.WithDefinitionStore();
+ // return builder;
+ //}
+
+ ///
+ /// Builds the WOWS replay data unpacker, registering any missing services.
+ ///
+ public void Build()
+ {
+ if (!replayDataParserAdded)
+ {
+ WithReplayDataParser();
+ }
+
+ if (!definitionStoreAdded)
+ {
+ WithDefinitionStore();
+ }
+
+ if (!definitionLoaderAdded)
+ {
+ WithDefinitionLoader();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Nodsoft.WowsReplaysUnpack/ServiceCollectionExtensions.cs b/Nodsoft.WowsReplaysUnpack/ServiceCollectionExtensions.cs
index 53d4023..e8ee16e 100644
--- a/Nodsoft.WowsReplaysUnpack/ServiceCollectionExtensions.cs
+++ b/Nodsoft.WowsReplaysUnpack/ServiceCollectionExtensions.cs
@@ -1,5 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
-using Nodsoft.WowsReplaysUnpack.Controllers;
using Nodsoft.WowsReplaysUnpack.Core.Definitions;
using Nodsoft.WowsReplaysUnpack.Services;
@@ -18,25 +17,28 @@ public static class ServiceCollectionExtensions
/// The service collection.
/// The service collection.
public static IServiceCollection AddWowsReplayUnpacker(this IServiceCollection services)
- => services.AddWowsReplayUnpacker();
-
+ => services.AddWowsReplayUnpacker();
+
///
/// Registers the WOWS replay data unpacker,
- /// using the specified and
+ /// using the specified , and
/// for parsing the replay data and definitions.
///
/// The service collection to add the services to.
/// The type of the replay data parser.
/// The type of the definition store.
+ /// The type of the definition loader.
/// The service collection.
- public static IServiceCollection AddWowsReplayUnpacker(this IServiceCollection services)
+ public static IServiceCollection AddWowsReplayUnpacker(this IServiceCollection services)
where TReplayDataParser : class, IReplayDataParser
where TDefinitionStore : class, IDefinitionStore
+ where TDefinitionLoader : class, IDefinitionLoader
=> services.AddWowsReplayUnpacker(unpacker =>
{
unpacker
.WithReplayDataParser()
- .WithDefinitionStore();
+ .WithDefinitionStore()
+ .WithDefinitionLoader();
}
);
@@ -56,78 +58,4 @@ public static IServiceCollection AddWowsReplayUnpacker(this IServiceCollection s
services.AddScoped();
return services;
}
-
- ///
- /// Provides a fluent API to build a WOWS replay data unpacker.
- ///
- public class ReplayUnpackerBuilder
- {
- private bool replayDataParserAdded;
- private bool definitionStoreAdded;
-
- public IServiceCollection Services { get; }
-
- ///
- /// Intializes a new instance of the class,
- /// by registering a as baseline.
- ///
- ///
- public ReplayUnpackerBuilder(IServiceCollection services)
- {
- Services = services;
- AddReplayController();
- }
-
- ///
- /// Registers a for use in the WOWS replay data unpacker.
- ///
- /// The type of the replay data parser.
- /// The builder.
- public ReplayUnpackerBuilder WithReplayDataParser() where TParser : class, IReplayDataParser
- {
- Services.AddScoped();
- replayDataParserAdded = true;
- return this;
- }
-
- ///
- /// Registers a for use in the WOWS replay data unpacker.
- ///
- /// The type of the replay controller.
- /// The builder.
- public ReplayUnpackerBuilder AddReplayController() where TController : class, IReplayController
- {
- Services.AddScoped>();
- Services.AddScoped();
- return this;
- }
-
- ///
- /// Registers a for use in the WOWS replay data unpacker.
- ///
- /// The type of the definition store.
- /// The builder.
- public ReplayUnpackerBuilder WithDefinitionStore() where TStore : class, IDefinitionStore
- {
- Services.AddSingleton();
- definitionStoreAdded = true;
- return this;
- }
-
- ///
- /// Builds the WOWS replay data unpacker, registering any missing services.
- ///
- public void Build()
- {
- if (!replayDataParserAdded)
- {
- WithReplayDataParser();
- }
-
- if (!definitionStoreAdded)
- {
- WithDefinitionStore();
- }
- }
- }
}
diff --git a/Nodsoft.WowsReplaysUnpack/Services/ReplayUnpackerService.cs b/Nodsoft.WowsReplaysUnpack/Services/ReplayUnpackerService.cs
index 5d699a0..f92b9b4 100644
--- a/Nodsoft.WowsReplaysUnpack/Services/ReplayUnpackerService.cs
+++ b/Nodsoft.WowsReplaysUnpack/Services/ReplayUnpackerService.cs
@@ -9,6 +9,7 @@
using System.IO.Compression;
using System.Text;
using System.Text.Json;
+using System.Threading;
namespace Nodsoft.WowsReplaysUnpack.Services;
@@ -22,6 +23,8 @@ public sealed class ReplayUnpackerService : ReplayUnpackerService,
private readonly JsonSerializerOptions _jsonSerializerOptions = new() { PropertyNameCaseInsensitive = true };
private readonly IReplayDataParser _replayDataParser;
private readonly IReplayController _replayController;
+ private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1);
+ private const int _semephoreTimeOut = 2000;
public ReplayUnpackerService(IReplayDataParser replayDataParser, TController replayController)
{
@@ -70,12 +73,13 @@ Seek to offset 4 in the replay file (skipping the magic number)
See http://wiki.vbaddict.net/pages/File_Replays for more details.
*/
options ??= new();
-
+ _semaphore.Wait(_semephoreTimeOut);
BinaryReader binaryReader = new(stream);
byte[] signature = binaryReader.ReadBytes(4);
int jsonBlockCount = binaryReader.ReadInt32();
+ _semaphore.Release();
// Verify replay signature
if (!signature.SequenceEqual(ReplaySignature))
{
@@ -86,11 +90,13 @@ Seek to offset 4 in the replay file (skipping the magic number)
// Read it and create the unpacked replay model
ArenaInfo arenaInfo = ReadJsonBlock(binaryReader);
UnpackedReplay replay = _replayController.CreateUnpackedReplay(arenaInfo);
+ _semaphore.Wait(_semephoreTimeOut);
ReadExtraJsonBlocks(replay, binaryReader, jsonBlockCount);
MemoryStream decryptedStream = new();
Decrypt(binaryReader, decryptedStream);
+ _semaphore.Release();
// Initial stream and reader not used anymore
binaryReader.Dispose();
@@ -100,14 +106,13 @@ Seek to offset 4 in the replay file (skipping the magic number)
// Decrypted stream not used anymore
decryptedStream.Dispose();
-
Version gameclientVersion = Version.Parse(arenaInfo.ClientVersionFromExe.Replace(',', '.'));
-
+ _semaphore.Wait(_semephoreTimeOut);
foreach (NetworkPacketBase networkPacket in _replayDataParser.ParseNetworkPackets(replayDataStream, options, gameclientVersion))
{
_replayController.HandleNetworkPacket(networkPacket, options);
}
-
+ _semaphore.Release();
return replay;
}
diff --git a/WoWS-ReplaysUnpack.sln b/WoWS-ReplaysUnpack.sln
index 966a679..cc4aa9b 100644
--- a/WoWS-ReplaysUnpack.sln
+++ b/WoWS-ReplaysUnpack.sln
@@ -24,6 +24,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Library", "Library", "{367B
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nodsoft.WowsReplaysUnpack.ExtendedData", "Nodsoft.WowsReplaysUnpack.ExtendedData\Nodsoft.WowsReplaysUnpack.ExtendedData.csproj", "{A9D51B91-8D9E-4E8E-A1A7-26BD5223EC53}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nodsoft.WowsReplaysUnpack.FileStore", "Nodsoft.WowsReplaysUnpack.FileStore\Nodsoft.WowsReplaysUnpack.FileStore.csproj", "{5278C904-639B-4368-A98C-6F7F79B1683E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -54,6 +56,10 @@ Global
{A9D51B91-8D9E-4E8E-A1A7-26BD5223EC53}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A9D51B91-8D9E-4E8E-A1A7-26BD5223EC53}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A9D51B91-8D9E-4E8E-A1A7-26BD5223EC53}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5278C904-639B-4368-A98C-6F7F79B1683E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5278C904-639B-4368-A98C-6F7F79B1683E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5278C904-639B-4368-A98C-6F7F79B1683E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5278C904-639B-4368-A98C-6F7F79B1683E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -62,6 +68,7 @@ Global
{B7A3AA8B-E20E-4DE9-B63A-A1A96741EFCD} = {367BF192-3EC3-4A87-B8E4-3E444E7019D0}
{18EF6FA2-5AF1-4277-81F4-7104BB35B3BD} = {367BF192-3EC3-4A87-B8E4-3E444E7019D0}
{A9D51B91-8D9E-4E8E-A1A7-26BD5223EC53} = {367BF192-3EC3-4A87-B8E4-3E444E7019D0}
+ {5278C904-639B-4368-A98C-6F7F79B1683E} = {367BF192-3EC3-4A87-B8E4-3E444E7019D0}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {26B5CF6B-2C64-417B-8149-3DFD8E93D497}