diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 53f90cb6e..49ecd3f7c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -218,7 +218,7 @@ jobs:
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
- $testProjects = Get-ChildItem -Path "GenHub/GenHub.Tests" -Recurse -Filter *.csproj | Where-Object { $_.Name -notlike '*Linux*' -and $_.Name -notlike '*MacOS*' }
+ $testProjects = Get-ChildItem -Path "GenHub/GenHub.Tests" -Recurse -Filter *.csproj | Where-Object { $_.Name -notlike '*Linux*' -and $_.Name -notlike '*MacOS*' -and $_.Name -notlike '*Performance*' }
if ($testProjects) {
foreach ($testProject in $testProjects) {
Write-Host "Testing $($testProject.FullName)"
@@ -358,7 +358,7 @@ jobs:
run: |
shopt -s globstar nullglob
for test_project in ${{ env.TEST_PROJECTS }}; do
- [[ "$test_project" == *Windows* || "$test_project" == *MacOS* ]] && continue
+ [[ "$test_project" == *Windows* || "$test_project" == *MacOS* || "$test_project" == *Performance* ]] && continue
echo "Testing $test_project"
dotnet test "$test_project" -c ${{ env.BUILD_CONFIGURATION }} --verbosity normal
done
@@ -493,17 +493,10 @@ jobs:
if kill -0 "$APP_PID" 2>/dev/null; then
echo "App stayed up for ${SURVIVED}s"
- if kill "$APP_PID" 2>/dev/null; then
- wait "$APP_PID" 2>/dev/null || true
- else
- set +e
- wait "$APP_PID"
- APP_STATUS=$?
- set -e
- echo "::error::GenHub.app exited before CI could stop it (status $APP_STATUS). Log follows."
- cat app-launch.log
- exit 1
- fi
+ kill "$APP_PID" 2>/dev/null || true
+ sleep 2
+ kill -9 "$APP_PID" 2>/dev/null || true
+ wait "$APP_PID" 2>/dev/null || true
else
set +e
wait "$APP_PID"
@@ -527,7 +520,7 @@ jobs:
while IFS= read -r test_project; do
# MacOS is covered by "Run macOS Tests" before publish, so it is skipped
# here rather than run a second time.
- [[ "$test_project" == *Windows* || "$test_project" == *Linux* || "$test_project" == *MacOS* ]] && continue
+ [[ "$test_project" == *Windows* || "$test_project" == *Linux* || "$test_project" == *MacOS* || "$test_project" == *Performance* ]] && continue
echo "Testing $test_project"
dotnet test "$test_project" -c ${{ env.BUILD_CONFIGURATION }} --verbosity normal
done < <(find GenHub/GenHub.Tests -type f -name '*.csproj' | sort)
diff --git a/.gitignore b/.gitignore
index 7237ca5d8..c6e67f390 100644
--- a/.gitignore
+++ b/.gitignore
@@ -184,3 +184,8 @@ GenHub/GenHub/SampleCatalogs/Subscribe-Test-Catalog.url
GenHub/GenHub/SampleCatalogs/Subscribe-Test-Catalog.desktop
GenHub/GenHub/SampleCatalogs/Subscribe-Test-Catalog.command
GenHub/GenHub/SampleCatalogs/Subscribe-Test-Catalog.webloc
+
+# Visual Studio metadata (exclude except AI context)
+.vs/*
+!.vs/Project-Overview.md
+!.vs/prompt.md
diff --git a/GenHub/Directory.Build.props b/GenHub/Directory.Build.props
index 73f102695..2ce971076 100644
--- a/GenHub/Directory.Build.props
+++ b/GenHub/Directory.Build.props
@@ -6,6 +6,9 @@
-->
0.0.1
+
+ $(NoWarn);SA0001;SA1101;SA1108;SA1116;SA1117;SA1124;SA1200;SA1202;SA1203;SA1204;SA1210;SA1309;SA1407;SA1413;SA1501;SA1503;SA1508;SA1515;SA1518;SA1600;SA1629;SA1633;SA1636;CS1591
+
-
+
-
@@ -56,4 +60,4 @@
-
+
\ No newline at end of file
diff --git a/GenHub/GenHub.Core/Constants/AssetPathConstants.cs b/GenHub/GenHub.Core/Constants/AssetPathConstants.cs
new file mode 100644
index 000000000..a79b248f8
--- /dev/null
+++ b/GenHub/GenHub.Core/Constants/AssetPathConstants.cs
@@ -0,0 +1,57 @@
+namespace GenHub.Core.Constants;
+
+///
+/// Constants for asset paths and resource URIs.
+///
+public static class AssetPathConstants
+{
+ ///
+ /// Legacy poster filename for China faction.
+ ///
+ public const string LegacyChinaPoster = "china-poster.png";
+
+ ///
+ /// Current cover filename for China faction.
+ ///
+ public const string ChinaCover = "china-cover.png";
+
+ ///
+ /// Legacy poster filename for USA faction.
+ ///
+ public const string LegacyUsaPoster = "usa-poster.png";
+
+ ///
+ /// Current cover filename for USA faction.
+ ///
+ public const string UsaCover = "usa-cover.png";
+
+ ///
+ /// Legacy poster filename for GLA faction.
+ ///
+ public const string LegacyGlaPoster = "gla-poster.png";
+
+ ///
+ /// Current cover filename for GLA faction.
+ ///
+ public const string GlaCover = "gla-cover.png";
+
+ ///
+ /// Legacy path for image assets.
+ ///
+ public const string LegacyImagesPath = "/Assets/Images/";
+
+ ///
+ /// Current path for cover assets.
+ ///
+ public const string CoversPath = "/Assets/Covers/";
+
+ ///
+ /// Avalonia resource URI scheme.
+ ///
+ public const string AvaresScheme = "avares://";
+
+ ///
+ /// Base Avalonia resource URI for GenHub application.
+ ///
+ public const string AvaresGenHubBase = "avares://GenHub/";
+}
diff --git a/GenHub/GenHub.Core/Constants/ErrorMessages.cs b/GenHub/GenHub.Core/Constants/ErrorMessages.cs
index ecbbe6be5..1b422b06e 100644
--- a/GenHub/GenHub.Core/Constants/ErrorMessages.cs
+++ b/GenHub/GenHub.Core/Constants/ErrorMessages.cs
@@ -34,4 +34,14 @@ public static class ErrorMessages
/// Error message for failed to process ZIP.
///
public const string FailedToProcessZip = "Failed to process ZIP: {0}";
+
+ ///
+ /// Error message when a profile requires a game installation.
+ ///
+ public const string ProfileRequiresGameInstallation = "• '{0}' requires a Game Installation";
+
+ ///
+ /// Error message when a profile requires a dependency.
+ ///
+ public const string ProfileRequiresDependency = "• '{0}' requires '{1}'";
}
diff --git a/GenHub/GenHub.Core/Constants/IoConstants.cs b/GenHub/GenHub.Core/Constants/IoConstants.cs
index 5b99c5710..bdbfadb35 100644
--- a/GenHub/GenHub.Core/Constants/IoConstants.cs
+++ b/GenHub/GenHub.Core/Constants/IoConstants.cs
@@ -6,9 +6,9 @@ namespace GenHub.Core.Constants;
public static class IoConstants
{
///
- /// Default buffer size for file operations (4KB).
+ /// Default buffer size for file operations (64KB).
///
- public const int DefaultFileBufferSize = 4096;
+ public const int DefaultFileBufferSize = 65536;
///
/// How many times a path may be re-resolved while following symbolic links whose targets are
diff --git a/GenHub/GenHub.Core/Constants/ModBuilderConstants.cs b/GenHub/GenHub.Core/Constants/ModBuilderConstants.cs
new file mode 100644
index 000000000..6f06383fb
--- /dev/null
+++ b/GenHub/GenHub.Core/Constants/ModBuilderConstants.cs
@@ -0,0 +1,156 @@
+using System.Collections.Generic;
+
+namespace GenHub.Core.Constants;
+
+///
+/// Constants for mod builder directory names, file names, default configurations, and pipeline stages.
+///
+public static class ModBuilderConstants
+{
+ ///
+ /// Default project file extension.
+ ///
+ public const string ProjectFileExtension = ".mbproj";
+
+ ///
+ /// File pattern for project selection dialogs.
+ ///
+ public const string ProjectFilePattern = "*.mbproj";
+
+ ///
+ /// Install manifest file name stored in target game directory.
+ ///
+ public const string InstallManifestFileName = ".modbuilder_install.json";
+
+ ///
+ /// Backup file extension used during file installation.
+ ///
+ public const string BackupFileExtension = ".modbuilder_backup";
+
+ ///
+ /// Default directory name for build output.
+ ///
+ public const string DefaultBuildDir = ".Build";
+
+ ///
+ /// Default directory name for release output.
+ ///
+ public const string DefaultReleaseDir = ".Release";
+
+ ///
+ /// Subdirectory name for raw bundle items within build directory.
+ ///
+ public const string RawBundleItemsSubdir = "raw_bundle_items";
+
+ ///
+ /// Subdirectory name for compiled big bundles within build directory.
+ ///
+ public const string BundlesSubdir = "bundles";
+
+ ///
+ /// Subdirectory name for bundle packs within build directory.
+ ///
+ public const string BundlePacksSubdir = "bundle_packs";
+
+ ///
+ /// Directory name for edited game source files.
+ ///
+ public const string GameFilesEditedDir = "GameFilesEdited";
+
+ ///
+ /// Directory name for project configuration files.
+ ///
+ public const string ConfigDir = "Configs";
+
+ ///
+ /// File name for bundle items configuration.
+ ///
+ public const string BundleItemsConfigFileName = "ModBundleItems.json";
+
+ ///
+ /// File name for bundle packs configuration.
+ ///
+ public const string BundlePacksConfigFileName = "ModBundlePacks.json";
+
+ ///
+ /// Directory name for uncompressed release files.
+ ///
+ public const string ReleaseFilesDir = "ReleaseFiles";
+
+ ///
+ /// Directory name for project resources.
+ ///
+ public const string ResourcesDir = "Resources";
+
+ ///
+ /// Subdirectory name for file hash registry files within resources.
+ ///
+ public const string FileHashRegistrySubdir = "FileHashRegistry";
+
+ ///
+ /// Default streaming threshold size in bytes (10MB).
+ ///
+ public const long DefaultStreamingThresholdBytes = 10 * 1024 * 1024;
+
+ ///
+ /// Name of the primary crunch tool executable.
+ ///
+ public const string CrunchExecutable = "crunch_x64.exe";
+
+ ///
+ /// Secondary fallback name of the crunch tool executable.
+ ///
+ public const string CrunchFallbackExecutable = "crunch.exe";
+
+ ///
+ /// DXT1 texture format identifier (no alpha).
+ ///
+ public const string Dxt1Format = "DXT1";
+
+ ///
+ /// DXT5 texture format identifier (with alpha).
+ ///
+ public const string Dxt5Format = "DXT5";
+
+ ///
+ /// Candidate search paths for the crunch tool executable.
+ ///
+ public static readonly IReadOnlyList CrunchExecutableCandidates =
+ [
+ @".tools\crunch_x64.exe",
+ @"tools\crunch_x64.exe",
+ @".tools\crunch.exe",
+ @"tools\crunch.exe",
+ ];
+
+ ///
+ /// Supported texture format flags for crunch.
+ ///
+ public static readonly IReadOnlyList CrunchTextureFormatFlags =
+ [
+ "-DXT1",
+ "-DXT2",
+ "-DXT3",
+ "-DXT4",
+ "-DXT5",
+ "-3DC",
+ "-DXN",
+ "-DXT5A",
+ "-DXT5_CCxY",
+ "-DXT5_xGxR",
+ "-DXT5_xGBR",
+ "-DXT5_AGBR",
+ "-DXT1A",
+ "-ETC1",
+ "-ETC2",
+ "-ETC2A",
+ "-ETC1S",
+ "-ETC2AS",
+ "-R8G8B8",
+ "-L8",
+ "-A8",
+ "-A8L8",
+ "-A8R8G8B8"
+ ];
+}
+
diff --git a/GenHub/GenHub.Core/Constants/ToolConstants.cs b/GenHub/GenHub.Core/Constants/ToolConstants.cs
index 129af5a47..ff3206e84 100644
--- a/GenHub/GenHub.Core/Constants/ToolConstants.cs
+++ b/GenHub/GenHub.Core/Constants/ToolConstants.cs
@@ -70,6 +70,52 @@ public static class ReplayManager
public static readonly string[] Tags = ["replays", "file-management", "sharing"];
}
+ ///
+ /// Constants for the ModBuilder tool plugin.
+ ///
+ public static class ModBuilder
+ {
+ ///
+ /// The unique identifier for the ModBuilder tool.
+ ///
+ public const string Id = "genhub.tools.modbuilder";
+
+ ///
+ /// The display name for the ModBuilder tool.
+ ///
+ public const string Name = "ModBuilder";
+
+ ///
+ /// The version of the ModBuilder tool.
+ ///
+ public const string Version = "1.0.0";
+
+ ///
+ /// The author of the ModBuilder tool.
+ ///
+ public const string Author = "GenHub Team";
+
+ ///
+ /// The description of the ModBuilder tool.
+ ///
+ public const string Description = "Build automation tool for Command & Conquer: Generals mods. Compile, package, and deploy your mod projects.";
+
+ ///
+ /// The icon path for the ModBuilder tool.
+ ///
+ public const string IconPath = "🔨";
+
+ ///
+ /// Whether the ModBuilder tool is bundled with the application.
+ ///
+ public const bool IsBundled = true;
+
+ ///
+ /// The tags associated with the ModBuilder tool.
+ ///
+ public static readonly string[] Tags = ["modding", "build-automation", "development"];
+ }
+
///
/// Mock path separator indicator for demo environments on Windows.
///
diff --git a/GenHub/GenHub.Core/Constants/UiConstants.cs b/GenHub/GenHub.Core/Constants/UiConstants.cs
index e3dc98279..e5c5f5d51 100644
--- a/GenHub/GenHub.Core/Constants/UiConstants.cs
+++ b/GenHub/GenHub.Core/Constants/UiConstants.cs
@@ -98,4 +98,21 @@ public static class UiConstants
/// Display name for Modding Tool content type.
///
public const string ModdingToolDisplayName = "Tools";
+
+ // Tab titles and descriptions
+
+ ///
+ /// Title for the Downloads tab.
+ ///
+ public const string DownloadsTabTitle = "Downloads";
+
+ ///
+ /// Description for the Downloads tab.
+ ///
+ public const string DownloadsTabDescription = "Manage your downloads and installations";
+
+ ///
+ /// Generic loading text displayed during async operations.
+ ///
+ public const string LoadingText = "Loading...";
}
diff --git a/GenHub/GenHub.Core/GenHub.Core.csproj b/GenHub/GenHub.Core/GenHub.Core.csproj
index 2dd9fe5dc..3f4de6862 100644
--- a/GenHub/GenHub.Core/GenHub.Core.csproj
+++ b/GenHub/GenHub.Core/GenHub.Core.csproj
@@ -10,6 +10,7 @@
+
diff --git a/GenHub/GenHub.Core/GlobalSuppressions.cs b/GenHub/GenHub.Core/GlobalSuppressions.cs
index e48c1e3fc..8090c7e3f 100644
--- a/GenHub/GenHub.Core/GlobalSuppressions.cs
+++ b/GenHub/GenHub.Core/GlobalSuppressions.cs
@@ -61,4 +61,32 @@
[assembly: SuppressMessage(
"StyleCop.CSharp.DocumentationRules",
"SA1633:File should have header",
- Justification = "Licensing and other information is provided in seperate files.")]
\ No newline at end of file
+ Justification = "Licensing and other information is provided in seperate files.")]
+
+[assembly: SuppressMessage(
+ "Design",
+ "CS-R1138:Inappropriate ordering of parameters",
+ Scope = "type",
+ Target = "~T:GenHub.Core.Models.Tools.ModBuilder.Converters.BundlePackListConverter",
+ Justification = "System.Text.Json requires ref Utf8JsonReader as the first parameter in JsonConverter.Read overrides.")]
+
+[assembly: SuppressMessage(
+ "StyleCop.CSharp.DocumentationRules",
+ "SA1649:FileNameMustMatchTypeName",
+ Scope = "type",
+ Target = "~T:GenHub.Core.Models.Tools.ModBuilder.PythonConfigRoot",
+ Justification = "PythonConfigModels.cs groups related DTO types.")]
+
+[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Scope = "type", Target = "~T:GenHub.Core.Models.Tools.ModBuilder.PythonBundlesConfig", Justification = "Python configuration DTOs are grouped in PythonConfigModels.cs.")]
+[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Scope = "type", Target = "~T:GenHub.Core.Models.Tools.ModBuilder.PythonBundleItem", Justification = "Python configuration DTOs are grouped in PythonConfigModels.cs.")]
+[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Scope = "type", Target = "~T:GenHub.Core.Models.Tools.ModBuilder.PythonBundlePack", Justification = "Python configuration DTOs are grouped in PythonConfigModels.cs.")]
+[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Scope = "type", Target = "~T:GenHub.Core.Models.Tools.ModBuilder.PythonBundleFileGroup", Justification = "Python configuration DTOs are grouped in PythonConfigModels.cs.")]
+[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Scope = "type", Target = "~T:GenHub.Core.Models.Tools.ModBuilder.PythonSourceTargetPair", Justification = "Python configuration DTOs are grouped in PythonConfigModels.cs.")]
+[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Scope = "type", Target = "~T:GenHub.Core.Models.Tools.ModBuilder.PythonBundleEvent", Justification = "Python configuration DTOs are grouped in PythonConfigModels.cs.")]
+[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Scope = "type", Target = "~T:GenHub.Core.Models.Tools.ModBuilder.PythonModJsonFilesConfig", Justification = "Python configuration DTOs are grouped in PythonConfigModels.cs.")]
+[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Scope = "type", Target = "~T:GenHub.Core.Models.Tools.ModBuilder.PythonModJsonFilesBuild", Justification = "Python configuration DTOs are grouped in PythonConfigModels.cs.")]
+[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Scope = "type", Target = "~T:GenHub.Core.Models.Tools.ModBuilder.PythonModFoldersConfig", Justification = "Python configuration DTOs are grouped in PythonConfigModels.cs.")]
+[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Scope = "type", Target = "~T:GenHub.Core.Models.Tools.ModBuilder.PythonModFoldersData", Justification = "Python configuration DTOs are grouped in PythonConfigModels.cs.")]
+[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Scope = "type", Target = "~T:GenHub.Core.Models.Tools.ModBuilder.SimplifiedConfigRoot", Justification = "Python configuration DTOs are grouped in PythonConfigModels.cs.")]
+[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Scope = "type", Target = "~T:GenHub.Core.Models.Tools.ModBuilder.SimplifiedBundleItem", Justification = "Python configuration DTOs are grouped in PythonConfigModels.cs.")]
+[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Scope = "type", Target = "~T:GenHub.Core.Models.Tools.ModBuilder.SimplifiedBundlePack", Justification = "Python configuration DTOs are grouped in PythonConfigModels.cs.")]
\ No newline at end of file
diff --git a/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/CommentStyle.cs b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/CommentStyle.cs
new file mode 100644
index 000000000..3f7985821
--- /dev/null
+++ b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/CommentStyle.cs
@@ -0,0 +1,22 @@
+namespace GenHub.Core.Interfaces.Tools.ModBuilder;
+
+///
+/// Comment styles for comment removal.
+///
+public enum CommentStyle
+{
+ ///
+ /// INI-style comments (semicolon).
+ ///
+ IniStyle,
+
+ ///
+ /// C-style comments (double slash).
+ ///
+ CStyle,
+
+ ///
+ /// Script-style comments (hash).
+ ///
+ ScriptStyle,
+}
diff --git a/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/ConversionOperationResult.cs b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/ConversionOperationResult.cs
new file mode 100644
index 000000000..e9bd48ecd
--- /dev/null
+++ b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/ConversionOperationResult.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using GenHub.Core.Models.Results;
+
+namespace GenHub.Core.Interfaces.Tools.ModBuilder;
+
+///
+/// Result of a conversion operation.
+///
+public class ConversionOperationResult : ResultBase
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ConversionOperationResult()
+ : base(true, (IEnumerable?)null, default)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Whether the operation succeeded.
+ /// The errors, if any.
+ /// The elapsed time.
+ public ConversionOperationResult(bool success, IEnumerable? errors = null, TimeSpan elapsed = default)
+ : base(success, errors, elapsed)
+ {
+ }
+
+ /// Creates a successful conversion operation result.
+ /// The elapsed time.
+ /// A successful .
+ public static ConversionOperationResult CreateSuccess(TimeSpan elapsed = default) => new(true, (IEnumerable?)null, elapsed);
+
+ /// Creates a failed conversion operation result with a single error message.
+ /// The error message.
+ /// The elapsed time.
+ /// A failed .
+ public static ConversionOperationResult CreateFailure(string error, TimeSpan elapsed = default) => new(false, [error], elapsed);
+
+ /// Creates a failed conversion operation result with multiple error messages.
+ /// The error messages.
+ /// The elapsed time.
+ /// A failed .
+ public static ConversionOperationResult CreateFailure(IEnumerable errors, TimeSpan elapsed = default) => new(false, errors, elapsed);
+}
diff --git a/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/ConversionOperationResult{T}.cs b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/ConversionOperationResult{T}.cs
new file mode 100644
index 000000000..e8247a7b6
--- /dev/null
+++ b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/ConversionOperationResult{T}.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Collections.Generic;
+using GenHub.Core.Models.Results;
+
+namespace GenHub.Core.Interfaces.Tools.ModBuilder;
+
+///
+/// Result of a conversion operation with data.
+///
+/// The type of data returned by the operation.
+public class ConversionOperationResult : ResultBase
+{
+ ///
+ /// Gets or sets the result data.
+ ///
+ public T? Data { get; set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ConversionOperationResult()
+ : base(true, (IEnumerable?)null, default)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Whether the operation succeeded.
+ /// The result data.
+ /// The errors, if any.
+ /// The elapsed time.
+ public ConversionOperationResult(bool success, T? data = default, IEnumerable? errors = null, TimeSpan elapsed = default)
+ : base(success, errors, elapsed)
+ {
+ Data = data;
+ }
+
+ /// Creates a successful conversion operation result with data.
+ /// The result data.
+ /// The elapsed time.
+ /// A successful .
+ public static ConversionOperationResult CreateSuccess(T data, TimeSpan elapsed = default) => new(true, data, (IEnumerable?)null, elapsed);
+
+ /// Creates a failed conversion operation result with a single error message.
+ /// The error message.
+ /// The elapsed time.
+ /// A failed .
+ public static ConversionOperationResult CreateFailure(string error, TimeSpan elapsed = default) => new(false, default, [error], elapsed);
+
+ /// Creates a failed conversion operation result with multiple error messages.
+ /// The error messages.
+ /// The elapsed time.
+ /// A failed .
+ public static ConversionOperationResult CreateFailure(IEnumerable errors, TimeSpan elapsed = default) => new(false, default, errors, elapsed);
+}
diff --git a/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IArchiveService.cs b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IArchiveService.cs
new file mode 100644
index 000000000..d4a20764f
--- /dev/null
+++ b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IArchiveService.cs
@@ -0,0 +1,77 @@
+using System;
+using System.IO.Compression;
+using System.Threading;
+using System.Threading.Tasks;
+using GenHub.Core.Models.Results;
+
+namespace GenHub.Core.Interfaces.Tools.ModBuilder;
+
+///
+/// Service for creating various archive formats (BIG, ZIP, TAR, TAR.GZ).
+///
+public interface IArchiveService
+{
+ ///
+ /// Creates a BIG archive from a source directory.
+ ///
+ /// Path to the source directory containing files to pack.
+ /// Path to the target .big file.
+ /// Optional progress reporter (0.0 to 1.0).
+ /// Cancellation token.
+ /// Operation result indicating success or failure.
+ Task> CreateBigArchiveAsync(
+ string sourceDirectory,
+ string targetBigPath,
+ IProgress? progress = null,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Creates a ZIP archive from a source directory with configurable compression.
+ ///
+ /// Path to the source directory containing files to pack.
+ /// Path to the target .zip file.
+ /// Compression level to use. Fastest for dev builds, Optimal for release builds.
+ /// Optional progress reporter (0.0 to 1.0).
+ /// Cancellation token.
+ /// Operation result indicating success or failure.
+ ///
+ /// Compression level trade-offs:
+ /// - NoCompression: Fastest, largest file size. Use for debugging only.
+ /// - Fastest: 20-30% faster than Optimal, slightly larger files. Recommended for dev builds.
+ /// - Optimal: Best compression ratio, slower. Recommended for release builds.
+ ///
+ Task> CreateZipArchiveAsync(
+ string sourceDirectory,
+ string targetZipPath,
+ CompressionLevel compressionLevel = CompressionLevel.Optimal,
+ IProgress? progress = null,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Creates a TAR archive from a source directory.
+ ///
+ /// Path to the source directory containing files to pack.
+ /// Path to the target .tar file.
+ /// Optional progress reporter (0.0 to 1.0).
+ /// Cancellation token.
+ /// Operation result indicating success or failure.
+ Task> CreateTarArchiveAsync(
+ string sourceDirectory,
+ string targetTarPath,
+ IProgress? progress = null,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Creates a TAR.GZ (gzipped tar) archive from a source directory.
+ ///
+ /// Path to the source directory containing files to pack.
+ /// Path to the target .tar.gz file.
+ /// Optional progress reporter (0.0 to 1.0).
+ /// Cancellation token.
+ /// Operation result indicating success or failure.
+ Task> CreateTarGzArchiveAsync(
+ string sourceDirectory,
+ string targetTarGzPath,
+ IProgress? progress = null,
+ CancellationToken cancellationToken = default);
+}
diff --git a/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IBuildCacheService.cs b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IBuildCacheService.cs
new file mode 100644
index 000000000..de87a82d0
--- /dev/null
+++ b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IBuildCacheService.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using GenHub.Core.Models.Tools.ModBuilder;
+
+namespace GenHub.Core.Interfaces.Tools.ModBuilder;
+
+///
+/// Manages build cache for change detection with MD5 hashing and modification time optimization.
+///
+public interface IBuildCacheService
+{
+ ///
+ /// Loads the previous build cache from disk.
+ ///
+ /// Path to the cache file (.json).
+ /// A cancellation token.
+ /// True if cache was loaded successfully.
+ Task LoadCacheAsync(string cachePath, CancellationToken cancellationToken = default);
+
+ ///
+ /// Saves the current build cache to disk.
+ ///
+ /// Path to the cache file (.json).
+ /// A cancellation token.
+ /// True if cache was saved successfully.
+ Task SaveCacheAsync(string cachePath, CancellationToken cancellationToken = default);
+
+ ///
+ /// Adds or updates a file in the new cache registry.
+ ///
+ /// The file path.
+ /// The file modification time.
+ /// The MD5 hash.
+ /// Build parameters.
+ void AddFile(string filePath, double modifiedTime, string md5, Dictionary? @params = null);
+
+ ///
+ /// Finds a file in the old cache registry.
+ ///
+ /// The file path.
+ /// The cached file info, or null if not found.
+ BuildFilePathInfo? FindOldFile(string filePath);
+
+ ///
+ /// Computes the MD5 hash for a file, with optimization to reuse cached hash if mtime unchanged.
+ ///
+ /// The file path.
+ /// A cancellation token.
+ /// The MD5 hash.
+ Task ComputeOrReuseMd5Async(string filePath, CancellationToken cancellationToken = default);
+
+ ///
+ /// Determines the change status of a file based on cache comparison.
+ ///
+ /// The file path.
+ /// The current MD5 hash.
+ /// Build parameters.
+ /// The build file status.
+ BuildFileStatus DetermineFileStatus(string filePath, string currentMd5, Dictionary? @params = null);
+
+ ///
+ /// Clears the current cache.
+ ///
+ void Clear();
+}
diff --git a/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IBuildEngineService.cs b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IBuildEngineService.cs
new file mode 100644
index 000000000..c1bd30eba
--- /dev/null
+++ b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IBuildEngineService.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using GenHub.Core.Models.Results.ModBuilder;
+using GenHub.Core.Models.Tools.ModBuilder;
+
+namespace GenHub.Core.Interfaces.Tools.ModBuilder;
+
+///
+/// Central orchestrator for the 5-stage ModBuilder build pipeline.
+/// Manages change detection, event system, and build execution.
+///
+public interface IBuildEngineService
+{
+ ///
+ /// Executes the build pipeline with the specified configuration.
+ ///
+ /// The ModBuilder project.
+ /// The build configuration.
+ /// The list of selected bundle pack names.
+ /// The build steps to execute (flags).
+ /// Optional progress reporter for build output.
+ /// A cancellation token.
+ /// A result indicating success or failure.
+ Task ExecuteBuildAsync(
+ ModBuilderProject project,
+ BuildConfiguration configuration,
+ List selectedBundlePacks,
+ BuildStep buildSteps,
+ IProgress? progress = null,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Checks if the build can be aborted.
+ ///
+ /// A cancellation token.
+ /// True if a build is currently running and can be aborted.
+ Task CanAbortAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Aborts the currently running build.
+ ///
+ /// A cancellation token.
+ /// A task representing the abort operation.
+ Task AbortAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Invalidates the cached build structure, forcing a rebuild on next access.
+ /// Call this when project configuration or files change.
+ ///
+ void InvalidateBuildStructureCache();
+}
diff --git a/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IConfigurationLoaderService.cs b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IConfigurationLoaderService.cs
new file mode 100644
index 000000000..b407b7165
--- /dev/null
+++ b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IConfigurationLoaderService.cs
@@ -0,0 +1,70 @@
+using GenHub.Core.Models.Tools.ModBuilder;
+
+namespace GenHub.Core.Interfaces.Tools.ModBuilder;
+
+///
+/// Service for loading and managing ModBuilder configuration files.
+///
+public interface IConfigurationLoaderService
+{
+ ///
+ /// Loads a single configuration file from the specified path.
+ ///
+ /// The absolute path to the configuration JSON file.
+ /// Cancellation token.
+ /// The loaded build configuration.
+ Task LoadConfigurationAsync(string configPath, CancellationToken cancellationToken = default);
+
+ ///
+ /// Loads and merges multiple configuration files.
+ /// Later configurations override earlier ones.
+ ///
+ /// The read-only list of configuration file paths to load.
+ /// Cancellation token.
+ /// The merged build configuration.
+ Task LoadAndMergeConfigurationsAsync(IReadOnlyList configPaths, CancellationToken cancellationToken = default);
+
+ ///
+ /// Resolves wildcard patterns in bundle file paths.
+ ///
+ /// The configuration containing wildcard patterns.
+ /// Cancellation token.
+ /// The configuration with resolved file paths.
+ Task ResolveWildcardsAsync(BuildConfiguration configuration, CancellationToken cancellationToken = default);
+
+ ///
+ /// Validates the configuration for correctness and completeness.
+ ///
+ /// The configuration to validate.
+ /// A list of validation errors, or empty if valid.
+ IReadOnlyList ValidateConfiguration(BuildConfiguration configuration);
+
+ ///
+ /// Loads the default embedded configuration.
+ ///
+ /// Cancellation token.
+ /// The default build configuration.
+ Task LoadDefaultConfigurationAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Merges two configurations, with the second overriding the first.
+ ///
+ /// The base configuration.
+ /// The configuration to merge on top.
+ /// The merged configuration.
+ BuildConfiguration MergeConfigurations(BuildConfiguration baseConfig, BuildConfiguration overrideConfig);
+
+ ///
+ /// Normalizes all paths in the configuration to use consistent separators.
+ ///
+ /// The configuration to normalize.
+ void NormalizePaths(BuildConfiguration configuration);
+
+ ///
+ /// Auto-discovers and loads configuration from standard project locations.
+ ///
+ /// The path to the project file (.mbproj).
+ /// Cancellation token.
+ /// The loaded build configuration, or null if no config found.
+ Task LoadProjectConfigurationAsync(string projectPath, CancellationToken cancellationToken = default);
+}
diff --git a/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IExternalToolService.cs b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IExternalToolService.cs
new file mode 100644
index 000000000..780e491a7
--- /dev/null
+++ b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IExternalToolService.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace GenHub.Core.Interfaces.Tools.ModBuilder;
+
+///
+/// Service for executing external tools (crunch, gametextcompiler, blender, etc.).
+///
+public interface IExternalToolService : IDisposable
+{
+ ///
+ /// Executes an external tool with the specified arguments.
+ ///
+ /// The path to the tool executable.
+ /// The command-line arguments.
+ /// Optional working directory.
+ /// Optional progress reporter for output.
+ /// Cancellation token.
+ /// A result indicating success or failure.
+ Task ExecuteToolAsync(
+ string toolPath,
+ string arguments,
+ string? workingDirectory = null,
+ IProgress? progress = null,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Validates that a tool exists and is executable.
+ ///
+ /// The path to the tool executable.
+ /// Cancellation token.
+ /// A result indicating whether the tool is valid.
+ Task> ValidateToolAsync(
+ string toolPath,
+ CancellationToken cancellationToken = default);
+}
diff --git a/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IFileConversionService.cs b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IFileConversionService.cs
new file mode 100644
index 000000000..1f06ec77f
--- /dev/null
+++ b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IFileConversionService.cs
@@ -0,0 +1,38 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace GenHub.Core.Interfaces.Tools.ModBuilder;
+
+///
+/// Service for coordinating file conversions across different formats.
+///
+public interface IFileConversionService
+{
+ ///
+ /// Converts a file from one format to another.
+ ///
+ /// The source file path.
+ /// The destination file path.
+ /// Optional conversion type hint.
+ /// Optional progress reporter.
+ /// Cancellation token.
+ /// A result indicating success or failure.
+ Task ConvertFileAsync(
+ string sourcePath,
+ string destinationPath,
+ string? conversionType = null,
+ System.IProgress? progress = null,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Validates whether a conversion is possible.
+ ///
+ /// The source file path.
+ /// The destination file path.
+ /// Cancellation token.
+ /// A result indicating whether the conversion is valid.
+ Task> ValidateConversionAsync(
+ string sourcePath,
+ string destinationPath,
+ CancellationToken cancellationToken = default);
+}
diff --git a/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IFileHashRegistryService.cs b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IFileHashRegistryService.cs
new file mode 100644
index 000000000..9e3b343ea
--- /dev/null
+++ b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IFileHashRegistryService.cs
@@ -0,0 +1,27 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace GenHub.Core.Interfaces.Tools.ModBuilder;
+
+///
+/// Service for managing file hash registry to skip processing of irrelevant files.
+/// Implements the FileHashRegistry optimization from Python ModBuilder.
+///
+public interface IFileHashRegistryService
+{
+ ///
+ /// Loads the hash registry from a CSV file.
+ ///
+ /// Path to the CSV file containing file hashes.
+ /// Cancellation token.
+ /// Task representing the async operation.
+ Task LoadRegistryAsync(string csvPath, CancellationToken cancellationToken = default);
+
+ ///
+ /// Checks if a file is irrelevant (unchanged from registry).
+ ///
+ /// Path to the file to check.
+ /// Current MD5 hash of the file.
+ /// True if the file matches the registry hash and can be skipped.
+ bool IsFileIrrelevant(string filePath, string currentMd5);
+}
diff --git a/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IImageConversionService.cs b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IImageConversionService.cs
new file mode 100644
index 000000000..680895a23
--- /dev/null
+++ b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IImageConversionService.cs
@@ -0,0 +1,43 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace GenHub.Core.Interfaces.Tools.ModBuilder;
+
+///
+/// Service for converting image files between various formats used in C&C Generals Zero Hour modding.
+/// Supports PSD, TGA, TIFF, DDS, and BMP formats with advanced features like multi-alpha compositing,
+/// resizing, and automatic DXT format selection.
+///
+public interface IImageConversionService
+{
+ ///
+ /// Converts an image from one format to another with optional processing parameters.
+ ///
+ /// Path to the source image file.
+ /// Path to the target image file.
+ /// Optional conversion parameters (resize, rescale, resampling, etc.).
+ /// Cancellation token.
+ /// True if conversion succeeded, false otherwise.
+ Task ConvertImageAsync(
+ string sourcePath,
+ string targetPath,
+ IDictionary? parameters = null,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Detects if an image has an alpha channel.
+ ///
+ /// Path to the image file.
+ /// Cancellation token.
+ /// True if the image has an alpha channel, false otherwise.
+ Task HasAlphaChannelAsync(string imagePath, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets the recommended DDS compression format (DXT1 or DXT5) based on alpha channel presence.
+ ///
+ /// Path to the image file.
+ /// Cancellation token.
+ /// Recommended DXT format string ("DXT1" or "DXT5").
+ Task GetRecommendedDxtFormatAsync(string imagePath, CancellationToken cancellationToken = default);
+}
diff --git a/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IMd5HashProvider.cs b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IMd5HashProvider.cs
new file mode 100644
index 000000000..94734a761
--- /dev/null
+++ b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IMd5HashProvider.cs
@@ -0,0 +1,18 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace GenHub.Core.Interfaces.Tools.ModBuilder;
+
+///
+/// Provides MD5 hash computation for files with modification time optimization.
+///
+public interface IMd5HashProvider
+{
+ ///
+ /// Computes the MD5 hash of a file asynchronously.
+ ///
+ /// The path to the file.
+ /// A cancellation token.
+ /// The MD5 hash as a lowercase hex string.
+ Task ComputeFileHashAsync(string filePath, CancellationToken cancellationToken = default);
+}
diff --git a/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IProjectConfigService.cs b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IProjectConfigService.cs
new file mode 100644
index 000000000..7c811e5d4
--- /dev/null
+++ b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IProjectConfigService.cs
@@ -0,0 +1,114 @@
+using GenHub.Core.Models.Results.ModBuilder;
+using GenHub.Core.Models.Tools.ModBuilder;
+
+namespace GenHub.Core.Interfaces.Tools.ModBuilder;
+
+///
+/// Service for managing ModBuilder project configurations (.mbproj files).
+///
+public interface IProjectConfigService
+{
+ ///
+ /// Creates a new ModBuilder project.
+ ///
+ /// The full path where the .mbproj file will be created.
+ /// The name of the project.
+ /// Optional game installation ID to associate with the project.
+ /// Optional project template to use.
+ /// Cancellation token.
+ /// A result containing the created project.
+ Task> CreateProjectAsync(
+ string projectPath,
+ string projectName,
+ string? gameInstallationId = null,
+ ProjectTemplate? template = null,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Loads an existing ModBuilder project from disk.
+ ///
+ /// The full path to the .mbproj file.
+ /// Whether to validate project integrity on load.
+ /// Cancellation token.
+ /// A result containing the loaded project.
+ Task> LoadProjectAsync(
+ string projectPath,
+ bool validateIntegrity = true,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Saves a ModBuilder project to disk.
+ ///
+ /// The full path to the .mbproj file.
+ /// The project to save.
+ /// Cancellation token.
+ /// A result indicating success or failure.
+ Task> SaveProjectAsync(
+ string projectPath,
+ ModBuilderProject project,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Validates a ModBuilder project's integrity.
+ ///
+ /// The full path to the .mbproj file.
+ /// The project to validate.
+ /// Cancellation token.
+ /// A result containing validation errors, if any.
+ Task> ValidateProjectAsync(
+ string projectPath,
+ ModBuilderProject project,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets the list of recent projects.
+ ///
+ /// Maximum number of recent projects to return.
+ /// Cancellation token.
+ /// A result containing the list of recent project paths.
+ Task>> GetRecentProjectsAsync(
+ int maxCount = 10,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Adds a project to the recent projects list.
+ ///
+ /// The full path to the .mbproj file.
+ /// Cancellation token.
+ /// A result indicating success or failure.
+ Task> AddToRecentProjectsAsync(
+ string projectPath,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Removes a project from the recent projects list.
+ ///
+ /// The full path to the .mbproj file.
+ /// Cancellation token.
+ /// A result indicating success or failure.
+ Task> RemoveFromRecentProjectsAsync(
+ string projectPath,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets the bundle configuration files for a project.
+ ///
+ /// The full path to the .mbproj file.
+ /// The project.
+ /// Cancellation token.
+ /// A result containing the list of bundle configuration file paths.
+ Task>> GetBundleConfigsAsync(
+ string projectPath,
+ ModBuilderProject project,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Updates the last build timestamp for a project.
+ ///
+ /// The full path to the .mbproj file.
+ /// Cancellation token.
+ /// A result indicating success or failure.
+ Task> UpdateLastBuildTimeAsync(
+ string projectPath,
+ CancellationToken cancellationToken = default);
+}
diff --git a/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IProjectStructureGenerator.cs b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IProjectStructureGenerator.cs
new file mode 100644
index 000000000..7591dc87a
--- /dev/null
+++ b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IProjectStructureGenerator.cs
@@ -0,0 +1,18 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace GenHub.Core.Interfaces.Tools.ModBuilder;
+
+///
+/// Service for generating complete project structure with folders and config files.
+///
+public interface IProjectStructureGenerator
+{
+ ///
+ /// Generates complete project structure including folders, config files, and README files.
+ ///
+ /// Path to the .mbproj file.
+ /// Cancellation token.
+ /// A task representing the asynchronous operation.
+ Task GenerateProjectStructureAsync(string projectPath, CancellationToken cancellationToken);
+}
diff --git a/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IStringTableConversionService.cs b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IStringTableConversionService.cs
new file mode 100644
index 000000000..8e88108e5
--- /dev/null
+++ b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/IStringTableConversionService.cs
@@ -0,0 +1,41 @@
+using System.Threading;
+using System.Threading.Tasks;
+using GenHub.Core.Models.Results;
+
+namespace GenHub.Core.Interfaces.Tools.ModBuilder;
+
+///
+/// Service for converting between CSF (game string table) and STR (text) formats.
+///
+public interface IStringTableConversionService
+{
+ ///
+ /// Converts a STR (text) file to CSF (game string table) format.
+ ///
+ /// Path to the source .str file.
+ /// Path to the target .csf file.
+ /// Optional language code (e.g., "en", "de", "fr").
+ /// Optional language code to swap and set in the CSF file.
+ /// Cancellation token.
+ /// Operation result indicating success or failure.
+ Task> ConvertStrToCsfAsync(
+ string sourceStrPath,
+ string targetCsfPath,
+ string? language = null,
+ string? swapAndSetLanguage = null,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Converts a CSF (game string table) file to STR (text) format.
+ ///
+ /// Path to the source .csf file.
+ /// Path to the target .str file.
+ /// Optional language code (e.g., "en", "de", "fr").
+ /// Cancellation token.
+ /// Operation result indicating success or failure.
+ Task> ConvertCsfToStrAsync(
+ string sourceCsfPath,
+ string targetStrPath,
+ string? language = null,
+ CancellationToken cancellationToken = default);
+}
diff --git a/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/ITextProcessingService.cs b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/ITextProcessingService.cs
new file mode 100644
index 000000000..4f63f8128
--- /dev/null
+++ b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/ITextProcessingService.cs
@@ -0,0 +1,81 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace GenHub.Core.Interfaces.Tools.ModBuilder;
+
+///
+/// Service for processing text files with various transformations.
+/// Supports line ending normalization, comment removal, whitespace optimization, and INI file processing.
+///
+public interface ITextProcessingService
+{
+ ///
+ /// Processes text content with multiple transformations based on options.
+ ///
+ /// The text content to process.
+ /// Processing options to apply.
+ /// Cancellation token.
+ /// Processed text content.
+ Task ProcessTextAsync(
+ string content,
+ TextProcessingOptions options,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Normalizes line endings to a specific format.
+ ///
+ /// The text content to normalize.
+ /// Target line ending type.
+ /// Cancellation token.
+ /// Text with normalized line endings.
+ Task NormalizeLineEndingsAsync(
+ string content,
+ LineEndingType type,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Removes comments from text content based on comment style.
+ ///
+ /// The text content to process.
+ /// Comment style to remove.
+ /// Cancellation token.
+ /// Text with comments removed.
+ Task RemoveCommentsAsync(
+ string content,
+ CommentStyle style,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Removes whitespace from text content based on mode.
+ ///
+ /// The text content to process.
+ /// Whitespace removal mode.
+ /// Cancellation token.
+ /// Text with whitespace removed.
+ Task RemoveWhitespaceAsync(
+ string content,
+ WhitespaceMode mode,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Removes sections of text enclosed between delimiter marker pairs.
+ ///
+ /// The text content to process.
+ /// List of [startMarker, endMarker] pairs to remove.
+ /// Cancellation token.
+ /// Text with delimited sections removed.
+ Task RemoveMarkersAsync(
+ string content,
+ IReadOnlyList> markers,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Optimizes INI files by removing comments, normalizing line endings, and cleaning whitespace.
+ ///
+ /// The INI file content to optimize.
+ /// Cancellation token.
+ /// Optimized INI file content.
+ Task OptimizeIniFileAsync(
+ string content,
+ CancellationToken cancellationToken = default);
+}
diff --git a/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/LineEndingType.cs b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/LineEndingType.cs
new file mode 100644
index 000000000..fc3a8d21a
--- /dev/null
+++ b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/LineEndingType.cs
@@ -0,0 +1,22 @@
+namespace GenHub.Core.Interfaces.Tools.ModBuilder;
+
+///
+/// Line ending types for text normalization.
+///
+public enum LineEndingType
+{
+ ///
+ /// Windows line endings (\r\n).
+ ///
+ CRLF,
+
+ ///
+ /// Unix/Linux line endings (\n).
+ ///
+ LF,
+
+ ///
+ /// Classic Mac line endings (\r).
+ ///
+ CR,
+}
diff --git a/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/TextProcessingOptions.cs b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/TextProcessingOptions.cs
new file mode 100644
index 000000000..b01cd4df2
--- /dev/null
+++ b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/TextProcessingOptions.cs
@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+
+namespace GenHub.Core.Interfaces.Tools.ModBuilder;
+
+///
+/// Options for text processing operations.
+///
+public class TextProcessingOptions
+{
+ ///
+ /// Gets or sets the line ending type to force. If null, line endings are not modified.
+ ///
+ public LineEndingType? ForceEOL { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to delete comments from the text.
+ ///
+ public bool DeleteComments { get; set; }
+
+ ///
+ /// Gets or sets the comment style to use when deleting comments.
+ ///
+ public CommentStyle CommentStyle { get; set; } = CommentStyle.IniStyle;
+
+ ///
+ /// Gets or sets a value indicating whether to delete whitespace from the text.
+ ///
+ public bool DeleteWhitespace { get; set; }
+
+ ///
+ /// Gets or sets the whitespace removal mode to use.
+ ///
+ public WhitespaceMode WhitespaceMode { get; set; } = WhitespaceMode.ExtraOnly;
+
+ ///
+ /// Gets or sets the list of delimiter marker pairs to strip out of the text.
+ ///
+ public IReadOnlyList>? ExcludeMarkersList { get; set; }
+}
diff --git a/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/ToolOperationResult.cs b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/ToolOperationResult.cs
new file mode 100644
index 000000000..a6e52bdf7
--- /dev/null
+++ b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/ToolOperationResult.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using GenHub.Core.Models.Results;
+
+namespace GenHub.Core.Interfaces.Tools.ModBuilder;
+
+///
+/// Result of a tool operation.
+///
+public class ToolOperationResult : ResultBase
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ToolOperationResult()
+ : base(true, (IEnumerable?)null, default)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Whether the operation succeeded.
+ /// Optional list of error messages.
+ /// The exit code of the tool process.
+ /// The elapsed duration of the operation.
+ public ToolOperationResult(bool success, IEnumerable? errors = null, int exitCode = 0, TimeSpan elapsed = default)
+ : base(success, errors, elapsed)
+ {
+ ExitCode = exitCode;
+ }
+
+ ///
+ /// Gets the tool exit code.
+ ///
+ public int ExitCode { get; init; }
+
+ ///
+ /// Creates a successful tool operation result.
+ ///
+ /// The process exit code.
+ /// The elapsed execution time.
+ /// A new successful instance.
+ public static ToolOperationResult CreateSuccess(int exitCode = 0, TimeSpan elapsed = default) =>
+ new(true, null, exitCode, elapsed);
+
+ ///
+ /// Creates a failed tool operation result.
+ ///
+ /// The failure error message.
+ /// The process exit code.
+ /// The elapsed execution time.
+ /// A new failed instance.
+ public static ToolOperationResult CreateFailure(string error, int exitCode = -1, TimeSpan elapsed = default) =>
+ new(false, [error], exitCode, elapsed);
+
+ ///
+ /// Creates a failed tool operation result with multiple errors.
+ ///
+ /// The collection of error messages.
+ /// The process exit code.
+ /// The elapsed execution time.
+ /// A new failed instance.
+ public static ToolOperationResult CreateFailure(IEnumerable errors, int exitCode = -1, TimeSpan elapsed = default) =>
+ new(false, errors, exitCode, elapsed);
+}
diff --git a/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/ToolOperationResult{T}.cs b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/ToolOperationResult{T}.cs
new file mode 100644
index 000000000..c4aa62f54
--- /dev/null
+++ b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/ToolOperationResult{T}.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using GenHub.Core.Models.Results;
+
+namespace GenHub.Core.Interfaces.Tools.ModBuilder;
+
+///
+/// Result of a tool operation with data.
+///
+/// The type of data returned by the operation.
+public class ToolOperationResult : ResultBase
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ToolOperationResult()
+ : base(true, (IEnumerable?)null, default)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Whether the operation succeeded.
+ /// The result payload data.
+ /// Optional list of error messages.
+ /// The exit code of the tool process.
+ /// The elapsed duration of the operation.
+ public ToolOperationResult(bool success, T? data = default, IEnumerable? errors = null, int exitCode = 0, TimeSpan elapsed = default)
+ : base(success, errors, elapsed)
+ {
+ Data = data;
+ ExitCode = exitCode;
+ }
+
+ ///
+ /// Gets the tool exit code.
+ ///
+ public int ExitCode { get; init; }
+
+ ///
+ /// Gets the result data.
+ ///
+ public T? Data { get; init; }
+
+ ///
+ /// Creates a successful tool operation result with data.
+ ///
+ /// The operation result data.
+ /// The process exit code.
+ /// The elapsed execution time.
+ /// A new successful instance with data.
+ public static ToolOperationResult CreateSuccess(T data, int exitCode = 0, TimeSpan elapsed = default) =>
+ new(true, data, null, exitCode, elapsed);
+
+ ///
+ /// Creates a failed tool operation result.
+ ///
+ /// The failure error message.
+ /// The process exit code.
+ /// The elapsed execution time.
+ /// A new failed instance.
+ public static ToolOperationResult CreateFailure(string error, int exitCode = -1, TimeSpan elapsed = default) =>
+ new(false, default, [error], exitCode, elapsed);
+
+ ///
+ /// Creates a failed tool operation result with multiple errors.
+ ///
+ /// The collection of error messages.
+ /// The process exit code.
+ /// The elapsed execution time.
+ /// A new failed instance.
+ public static ToolOperationResult CreateFailure(IEnumerable errors, int exitCode = -1, TimeSpan elapsed = default) =>
+ new(false, default, errors, exitCode, elapsed);
+}
diff --git a/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/WhitespaceMode.cs b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/WhitespaceMode.cs
new file mode 100644
index 000000000..3a6db05fb
--- /dev/null
+++ b/GenHub/GenHub.Core/Interfaces/Tools/ModBuilder/WhitespaceMode.cs
@@ -0,0 +1,32 @@
+namespace GenHub.Core.Interfaces.Tools.ModBuilder;
+
+///
+/// Whitespace removal modes.
+///
+public enum WhitespaceMode
+{
+ ///
+ /// Remove leading whitespace from lines.
+ ///
+ Leading,
+
+ ///
+ /// Remove trailing whitespace from lines.
+ ///
+ Trailing,
+
+ ///
+ /// Remove empty lines.
+ ///
+ EmptyLines,
+
+ ///
+ /// Remove extra whitespace (multiple spaces to single space).
+ ///
+ ExtraOnly,
+
+ ///
+ /// Remove all extra whitespace (trim lines).
+ ///
+ All,
+}
diff --git a/GenHub/GenHub.Core/Models/Common/UserSettings.cs b/GenHub/GenHub.Core/Models/Common/UserSettings.cs
index 83cdb2a76..00019de13 100644
--- a/GenHub/GenHub.Core/Models/Common/UserSettings.cs
+++ b/GenHub/GenHub.Core/Models/Common/UserSettings.cs
@@ -106,6 +106,11 @@ public class UserSettings
///
public HashSet ExecutedInstallationSteps { get; set; } = [];
+ ///
+ /// Gets or sets the CSV catalog configuration.
+ ///
+ public CsvCatalogConfiguration? CsvCatalogConfiguration { get; set; }
+
/// Marks a property as explicitly set by the user.
/// The name of the property to mark as explicitly set.
public void MarkAsExplicitlySet(string propertyName)
@@ -218,6 +223,7 @@ public UserSettings Clone()
ExplicitlySetProperties = [.. ExplicitlySetProperties],
CasConfiguration = (CasConfiguration?)CasConfiguration?.Clone() ?? new CasConfiguration(),
ExecutedInstallationSteps = ExecutedInstallationSteps != null ? [.. ExecutedInstallationSteps] : [],
+ CsvCatalogConfiguration = CsvCatalogConfiguration,
SkippedUpdateVersions = SkippedUpdateVersions != null ? new Dictionary(SkippedUpdateVersions) : [],
PreferredUpdateStrategy = PreferredUpdateStrategy,
PublisherSubscriptions = PublisherSubscriptions != null
diff --git a/GenHub/GenHub.Core/Models/Results/ModBuilder/BuildOperationResult.cs b/GenHub/GenHub.Core/Models/Results/ModBuilder/BuildOperationResult.cs
new file mode 100644
index 000000000..1c8c0cadd
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Results/ModBuilder/BuildOperationResult.cs
@@ -0,0 +1,144 @@
+using System;
+using System.Collections.Generic;
+
+namespace GenHub.Core.Models.Results.ModBuilder;
+
+///
+/// Represents the result of a build operation, deriving from ResultBase.
+///
+public class BuildOperationResult : ResultBase
+{
+ ///
+ /// Initializes a new instance of the class with multiple errors.
+ ///
+ /// Whether the build succeeded.
+ /// Any error messages.
+ /// Time taken for the build operation.
+ /// Number of files processed.
+ /// Number of files skipped.
+ /// Number of files failed.
+ public BuildOperationResult(
+ bool success,
+ IEnumerable? errors = null,
+ TimeSpan elapsed = default,
+ int filesProcessed = 0,
+ int filesSkipped = 0,
+ int filesFailed = 0)
+ : base(success, errors, elapsed)
+ {
+ FilesProcessed = filesProcessed;
+ FilesSkipped = filesSkipped;
+ FilesFailed = filesFailed;
+ }
+
+ ///
+ /// Initializes a new instance of the class with a single error.
+ ///
+ /// Whether the build succeeded.
+ /// A single error message.
+ /// Time taken for the build operation.
+ /// Number of files processed.
+ /// Number of files skipped.
+ /// Number of files failed.
+ public BuildOperationResult(
+ bool success,
+ string? error,
+ TimeSpan elapsed = default,
+ int filesProcessed = 0,
+ int filesSkipped = 0,
+ int filesFailed = 0)
+ : base(success, error, elapsed)
+ {
+ FilesProcessed = filesProcessed;
+ FilesSkipped = filesSkipped;
+ FilesFailed = filesFailed;
+ }
+
+ ///
+ /// Gets the number of files processed.
+ ///
+ public int FilesProcessed { get; init; }
+
+ ///
+ /// Gets the number of files failed.
+ ///
+ public int FilesFailed { get; init; }
+
+ ///
+ /// Gets the number of files skipped (unchanged).
+ ///
+ public int FilesSkipped { get; init; }
+
+ ///
+ /// Creates a successful build operation result.
+ ///
+ /// Number of files processed.
+ /// Number of files skipped.
+ /// Number of files failed.
+ /// Time taken for the build operation.
+ /// A successful build operation result.
+ public static BuildOperationResult CreateSuccess(
+ int filesProcessed = 0,
+ int filesSkipped = 0,
+ int filesFailed = 0,
+ TimeSpan elapsed = default)
+ {
+ return new BuildOperationResult(
+ success: true,
+ errors: null,
+ elapsed: elapsed,
+ filesProcessed: filesProcessed,
+ filesSkipped: filesSkipped,
+ filesFailed: filesFailed);
+ }
+
+ ///
+ /// Creates a failed build operation result with error messages.
+ ///
+ /// Collection of error messages.
+ /// Number of files processed.
+ /// Number of files skipped.
+ /// Number of files failed.
+ /// Time taken for the build operation.
+ /// A failed build operation result.
+ public static BuildOperationResult CreateFailure(
+ IEnumerable errors,
+ int filesProcessed = 0,
+ int filesSkipped = 0,
+ int filesFailed = 0,
+ TimeSpan elapsed = default)
+ {
+ return new BuildOperationResult(
+ success: false,
+ errors: errors,
+ elapsed: elapsed,
+ filesProcessed: filesProcessed,
+ filesSkipped: filesSkipped,
+ filesFailed: filesFailed);
+ }
+
+ ///
+ /// Creates a failed build operation result with a single error message.
+ ///
+ /// The error message.
+ /// Number of files processed.
+ /// Number of files skipped.
+ /// Number of files failed.
+ /// Time taken for the build operation.
+ /// A failed build operation result.
+ public static BuildOperationResult CreateFailure(
+ string errorMessage,
+ int filesProcessed = 0,
+ int filesSkipped = 0,
+ int filesFailed = 0,
+ TimeSpan elapsed = default)
+ {
+ return new BuildOperationResult(
+ success: false,
+ error: errorMessage,
+ elapsed: elapsed,
+ filesProcessed: filesProcessed,
+ filesSkipped: filesSkipped,
+ filesFailed: filesFailed);
+ }
+}
diff --git a/GenHub/GenHub.Core/Models/Results/ModBuilder/CacheOperationResult.cs b/GenHub/GenHub.Core/Models/Results/ModBuilder/CacheOperationResult.cs
new file mode 100644
index 000000000..f2b023def
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Results/ModBuilder/CacheOperationResult.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using GenHub.Core.Models.Results;
+
+namespace GenHub.Core.Models.Results.ModBuilder;
+
+///
+/// Represents the result of a cache operation.
+///
+public class CacheOperationResult : ResultBase
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CacheOperationResult()
+ : base(true, (IEnumerable?)null, default)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Whether the operation succeeded.
+ /// The errors, if any.
+ /// The elapsed time.
+ public CacheOperationResult(bool success, IEnumerable? errors = null, TimeSpan elapsed = default)
+ : base(success, errors, elapsed)
+ {
+ }
+
+ /// Creates a successful cache operation result.
+ /// The elapsed time.
+ /// A successful .
+ public static CacheOperationResult CreateSuccess(TimeSpan elapsed = default) => new(true, (IEnumerable?)null, elapsed);
+
+ /// Creates a failed cache operation result with a single error message.
+ /// The error message.
+ /// The elapsed time.
+ /// A failed .
+ public static CacheOperationResult CreateFailure(string error, TimeSpan elapsed = default) => new(false, [error], elapsed);
+
+ /// Creates a failed cache operation result with multiple error messages.
+ /// The error messages.
+ /// The elapsed time.
+ /// A failed .
+ public static CacheOperationResult CreateFailure(IEnumerable errors, TimeSpan elapsed = default) => new(false, errors, elapsed);
+}
diff --git a/GenHub/GenHub.Core/Models/Results/ModBuilder/CacheOperationResult{T}.cs b/GenHub/GenHub.Core/Models/Results/ModBuilder/CacheOperationResult{T}.cs
new file mode 100644
index 000000000..e3a83bf6d
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Results/ModBuilder/CacheOperationResult{T}.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Collections.Generic;
+using GenHub.Core.Models.Results;
+
+namespace GenHub.Core.Models.Results.ModBuilder;
+
+///
+/// Represents the result of a cache operation with data.
+///
+/// The type of data returned by the operation.
+public class CacheOperationResult : ResultBase
+{
+ ///
+ /// Gets or sets the result data.
+ ///
+ public T? Data { get; set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CacheOperationResult()
+ : base(true, (IEnumerable?)null, default)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Whether the operation succeeded.
+ /// The result data.
+ /// The errors, if any.
+ /// The elapsed time.
+ public CacheOperationResult(bool success, T? data = default, IEnumerable? errors = null, TimeSpan elapsed = default)
+ : base(success, errors, elapsed)
+ {
+ Data = data;
+ }
+
+ /// Creates a successful cache operation result with data.
+ /// The result data.
+ /// The elapsed time.
+ /// A successful .
+ public static CacheOperationResult CreateSuccess(T data, TimeSpan elapsed = default) => new(true, data, (IEnumerable?)null, elapsed);
+
+ /// Creates a failed cache operation result with a single error message.
+ /// The error message.
+ /// The elapsed time.
+ /// A failed .
+ public static CacheOperationResult CreateFailure(string error, TimeSpan elapsed = default) => new(false, default, [error], elapsed);
+
+ /// Creates a failed cache operation result with multiple error messages.
+ /// The error messages.
+ /// The elapsed time.
+ /// A failed .
+ public static CacheOperationResult CreateFailure(IEnumerable errors, TimeSpan elapsed = default) => new(false, default, errors, elapsed);
+}
diff --git a/GenHub/GenHub.Core/Models/Results/ModBuilder/ProjectOperationResult.cs b/GenHub/GenHub.Core/Models/Results/ModBuilder/ProjectOperationResult.cs
new file mode 100644
index 000000000..98969a90c
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Results/ModBuilder/ProjectOperationResult.cs
@@ -0,0 +1,77 @@
+namespace GenHub.Core.Models.Results.ModBuilder;
+
+///
+/// Represents the result of a ModBuilder project operation.
+///
+/// The type of data returned by the operation.
+public class ProjectOperationResult : OperationResult
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Whether the operation succeeded.
+ /// The data returned by the operation.
+ /// The errors, if any.
+ /// Validation errors, if any.
+ /// The elapsed time.
+ protected ProjectOperationResult(
+ bool success,
+ T? data,
+ IEnumerable? errors = null,
+ IEnumerable? validationErrors = null,
+ TimeSpan elapsed = default)
+ : base(success, data, errors, elapsed)
+ {
+ ValidationErrors = validationErrors?.ToList().AsReadOnly() ?? new List().AsReadOnly();
+ }
+
+ ///
+ /// Gets the validation errors, if any.
+ ///
+ public IReadOnlyList ValidationErrors { get; }
+
+ ///
+ /// Gets a value indicating whether there are validation errors.
+ ///
+ public bool HasValidationErrors => ValidationErrors.Count > 0;
+
+ ///
+ /// Creates a successful project operation result.
+ ///
+ /// The data returned by the operation.
+ /// The elapsed time.
+ /// A successful .
+ public static new ProjectOperationResult CreateSuccess(T data, TimeSpan elapsed = default)
+ => new(true, data, null, null, elapsed);
+
+ ///
+ /// Creates a failed project operation result with a single error message.
+ ///
+ /// The error message.
+ /// The elapsed time.
+ /// A failed .
+ public static new ProjectOperationResult CreateFailure(string error, TimeSpan elapsed = default)
+ => new(false, default, new[] { error }, null, elapsed);
+
+ ///
+ /// Creates a failed project operation result with multiple error messages.
+ ///
+ /// The error messages.
+ /// The elapsed time.
+ /// A failed .
+ public static new ProjectOperationResult CreateFailure(IEnumerable errors, TimeSpan elapsed = default)
+ => new(false, default, errors, null, elapsed);
+
+ ///
+ /// Creates a failed project operation result with validation errors.
+ ///
+ /// The error message.
+ /// The validation errors.
+ /// The elapsed time.
+ /// A failed .
+ public static ProjectOperationResult CreateValidationFailure(
+ string error,
+ IEnumerable validationErrors,
+ TimeSpan elapsed = default)
+ => new(false, default, new[] { error }, validationErrors, elapsed);
+}
diff --git a/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildConfiguration.cs b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildConfiguration.cs
new file mode 100644
index 000000000..8230d5e03
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildConfiguration.cs
@@ -0,0 +1,60 @@
+using System.IO.Compression;
+using System.Text.Json.Serialization;
+using GenHub.Core.Models.Tools.ModBuilder.Converters;
+
+namespace GenHub.Core.Models.Tools.ModBuilder;
+
+///
+/// Represents the complete build configuration loaded from JSON files.
+///
+public class BuildConfiguration
+{
+ ///
+ /// Gets or sets the list of bundle items to build.
+ ///
+ [JsonPropertyName("items")]
+ public List Items { get; set; } = new();
+
+ ///
+ /// Gets or sets the list of bundle packs for distribution.
+ ///
+ [JsonPropertyName("packs")]
+ [JsonConverter(typeof(BundlePackListConverter))]
+ public List Packs { get; set; } = new();
+
+ ///
+ /// Gets or sets the folder configuration for build outputs.
+ ///
+ [JsonPropertyName("folders")]
+ public FolderConfiguration Folders { get; set; } = new();
+
+ ///
+ /// Gets or sets the game runner configuration.
+ ///
+ [JsonPropertyName("runner")]
+ public RunnerConfiguration Runner { get; set; } = new();
+
+ ///
+ /// Gets or sets the external tools configuration.
+ ///
+ [JsonPropertyName("tools")]
+ public Dictionary Tools { get; set; } = new();
+
+ ///
+ /// Gets or sets the compression level for ZIP archives.
+ ///
+ ///
+ /// Defaults to Fastest for better dev build performance.
+ /// Use Optimal for release builds to minimize file size.
+ /// Use NoCompression for debugging archive issues.
+ ///
+ [JsonPropertyName("compressionLevel")]
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ public CompressionLevel ZipCompressionLevel { get; set; } = CompressionLevel.Fastest;
+
+ ///
+ /// Gets or sets the configuration file paths that were loaded.
+ ///
+ [JsonIgnore]
+ public List LoadedConfigFiles { get; set; } = new();
+}
diff --git a/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildFilePathInfo.cs b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildFilePathInfo.cs
new file mode 100644
index 000000000..19f8c86c6
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildFilePathInfo.cs
@@ -0,0 +1,71 @@
+using System.Collections.Generic;
+using MessagePack;
+
+namespace GenHub.Core.Models.Tools.ModBuilder;
+
+///
+/// Represents file metadata for change detection.
+/// Serializable dataclass for build state persistence.
+///
+[MessagePackObject]
+public sealed class BuildFilePathInfo
+{
+ ///
+ /// Gets or sets the file path.
+ ///
+ [Key(0)]
+ public string Path { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the file modification time (Unix timestamp).
+ ///
+ [Key(1)]
+ public double ModifiedTime { get; set; }
+
+ ///
+ /// Gets or sets the MD5 hash of the file.
+ ///
+ [Key(2)]
+ public string Md5 { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the build parameters associated with this file.
+ ///
+ [Key(3)]
+ public Dictionary? Params { get; set; }
+
+ ///
+ /// Checks if this file info matches another based on MD5 and params.
+ ///
+ /// The other file info to compare with.
+ /// True if the file info matches; otherwise, false.
+ public bool Matches(BuildFilePathInfo? other)
+ {
+ if (other == null)
+ return false;
+
+ if (Md5 != other.Md5)
+ return false;
+
+ // Compare params dictionaries
+ if (Params == null && other.Params == null)
+ return true;
+
+ if (Params == null || other.Params == null)
+ return false;
+
+ if (Params.Count != other.Params.Count)
+ return false;
+
+ foreach (var kvp in Params)
+ {
+ if (!other.Params.TryGetValue(kvp.Key, out var otherValue))
+ return false;
+
+ if (!Equals(kvp.Value, otherValue))
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildFileStatus.cs b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildFileStatus.cs
new file mode 100644
index 000000000..127a4ace0
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildFileStatus.cs
@@ -0,0 +1,42 @@
+namespace GenHub.Core.Models.Tools.ModBuilder;
+
+///
+/// Represents the change detection status of a file in the build system.
+///
+public enum BuildFileStatus
+{
+ ///
+ /// Status has not been determined yet.
+ ///
+ Unknown,
+
+ ///
+ /// File is marked as irrelevant by the file hash registry.
+ ///
+ Irrelevant,
+
+ ///
+ /// File exists and has not changed since the last build.
+ ///
+ Unchanged,
+
+ ///
+ /// File was removed from the source.
+ ///
+ Removed,
+
+ ///
+ /// File is expected but missing from the source.
+ ///
+ Missing,
+
+ ///
+ /// File is new and was not present in the previous build.
+ ///
+ Added,
+
+ ///
+ /// File exists but has been modified since the last build.
+ ///
+ Changed,
+}
diff --git a/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildFileType.cs b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildFileType.cs
new file mode 100644
index 000000000..a0b502666
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildFileType.cs
@@ -0,0 +1,92 @@
+namespace GenHub.Core.Models.Tools.ModBuilder;
+
+///
+/// Represents file types supported by the ModBuilder conversion system.
+///
+public enum BuildFileType
+{
+ ///
+ /// Generals .big archive format.
+ ///
+ Big,
+
+ ///
+ /// Blender 3D model file (.blend).
+ ///
+ Blend,
+
+ ///
+ /// Bitmap image file (.bmp).
+ ///
+ Bmp,
+
+ ///
+ /// Compiled String File - game string table (.csf).
+ ///
+ Csf,
+
+ ///
+ /// DirectDraw Surface texture file (.dds).
+ ///
+ Dds,
+
+ ///
+ /// Gzip compressed archive (.gz).
+ ///
+ Gz,
+
+ ///
+ /// INI configuration file (.ini).
+ ///
+ Ini,
+
+ ///
+ /// Photoshop document (.psd).
+ ///
+ Psd,
+
+ ///
+ /// String table text file (.str).
+ ///
+ Str,
+
+ ///
+ /// Tar archive file (.tar).
+ ///
+ Tar,
+
+ ///
+ /// Targa image file (.tga).
+ ///
+ Tga,
+
+ ///
+ /// Tagged Image File Format (.tiff).
+ ///
+ Tiff,
+
+ ///
+ /// Westwood 3D model file (.w3d).
+ ///
+ W3d,
+
+ ///
+ /// Window definition file (.wnd).
+ ///
+ Wnd,
+
+ ///
+ /// ZIP archive file (.zip).
+ ///
+ Zip,
+
+ ///
+ /// Matches any file type.
+ ///
+ Any,
+
+ ///
+ /// Automatically determine file type from extension.
+ ///
+ Auto,
+}
diff --git a/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildIndex.cs b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildIndex.cs
new file mode 100644
index 000000000..6b65b1584
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildIndex.cs
@@ -0,0 +1,32 @@
+namespace GenHub.Core.Models.Tools.ModBuilder;
+
+///
+/// Represents the 5-stage build pipeline index for the ModBuilder system.
+///
+public enum BuildIndex
+{
+ ///
+ /// Stage 1: Process source files with format conversions.
+ ///
+ RawBundleItem = 0,
+
+ ///
+ /// Stage 2: Package processed files into .big archives.
+ ///
+ BigBundleItem = 1,
+
+ ///
+ /// Stage 3: Group bundle items into packs.
+ ///
+ RawBundlePack = 2,
+
+ ///
+ /// Stage 4: Create distribution archives (.zip) for release.
+ ///
+ ReleaseBundlePack = 3,
+
+ ///
+ /// Stage 5: Install bundle packs to the game directory.
+ ///
+ InstallBundlePack = 4,
+}
diff --git a/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildProgress.cs b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildProgress.cs
new file mode 100644
index 000000000..f699ef1b8
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildProgress.cs
@@ -0,0 +1,100 @@
+using System;
+
+namespace GenHub.Core.Models.Tools.ModBuilder;
+
+///
+/// Represents the current stage of the build process.
+///
+public enum BuildStage
+{
+ ///
+ /// Loading configuration and initializing build structure.
+ ///
+ Loading,
+
+ ///
+ /// Processing and converting source files.
+ ///
+ Processing,
+
+ ///
+ /// Converting images and other assets.
+ ///
+ Converting,
+
+ ///
+ /// Creating archive files (.big, .zip).
+ ///
+ Archiving,
+
+ ///
+ /// Build completed successfully.
+ ///
+ Complete,
+}
+
+///
+/// Represents progress information during a build operation.
+///
+public class BuildProgress
+{
+ ///
+ /// Gets or sets the current build step description.
+ ///
+ public string CurrentStep { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the current build index (stage).
+ ///
+ public BuildIndex? CurrentIndex { get; set; }
+
+ ///
+ /// Gets or sets the current build stage.
+ ///
+ public BuildStage CurrentStage { get; set; }
+
+ ///
+ /// Gets or sets the current file being processed.
+ ///
+ public string CurrentFile { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets an optional message describing the current operation.
+ ///
+ public string? Message { get; set; }
+
+ ///
+ /// Gets or sets the number of files processed.
+ ///
+ public int ProcessedFiles { get; set; }
+
+ ///
+ /// Gets or sets the total number of files to process.
+ ///
+ public int TotalFiles { get; set; }
+
+ ///
+ /// Gets or sets the progress percentage (0.0 to 100.0).
+ ///
+ public double PercentComplete { get; set; }
+
+ ///
+ /// Gets or sets the estimated time remaining.
+ ///
+ public TimeSpan? EstimatedTimeRemaining { get; set; }
+
+ ///
+ /// Gets or sets the number of items processed (legacy).
+ ///
+ public int ProcessedItems { get; set; }
+
+ ///
+ /// Gets or sets the total number of items (legacy).
+ ///
+ public int TotalItems { get; set; }
+
+ ///
+ /// Gets or sets the progress percentage (0.0 to 1.0) (legacy).
+ ///
+ public double Percentage { get; set; }
+}
diff --git a/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildResult.cs b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildResult.cs
new file mode 100644
index 000000000..15f2161a5
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildResult.cs
@@ -0,0 +1,108 @@
+using GenHub.Core.Models.Results;
+
+namespace GenHub.Core.Models.Tools.ModBuilder;
+
+///
+/// Represents the result of a build operation.
+///
+public class BuildResult : ResultBase
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Whether the build was successful.
+ /// Any errors that occurred.
+ /// Time taken for the build.
+ public BuildResult(bool success, IEnumerable? errors = null, TimeSpan elapsed = default)
+ : base(success, errors, elapsed)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Whether the build was successful.
+ /// A single error message.
+ /// Time taken for the build.
+ public BuildResult(bool success, string? error = null, TimeSpan elapsed = default)
+ : base(success, error, elapsed)
+ {
+ }
+
+ ///
+ /// Gets or sets the number of files processed.
+ ///
+ public int FilesProcessed { get; set; }
+
+ ///
+ /// Gets or sets the number of files that were unchanged.
+ ///
+ public int FilesUnchanged { get; set; }
+
+ ///
+ /// Gets or sets the number of files that were added.
+ ///
+ public int FilesAdded { get; set; }
+
+ ///
+ /// Gets or sets the number of files that were changed.
+ ///
+ public int FilesChanged { get; set; }
+
+ ///
+ /// Gets or sets the number of files that were removed.
+ ///
+ public int FilesRemoved { get; set; }
+
+ ///
+ /// Gets or sets the build steps that were executed.
+ ///
+ public BuildStep StepsExecuted { get; set; }
+
+ ///
+ /// Gets or sets the list of bundle items that were built.
+ ///
+ public List BuiltItems { get; set; } = new();
+
+ ///
+ /// Gets or sets the list of bundle packs that were created.
+ ///
+ public List CreatedPacks { get; set; } = new();
+
+ ///
+ /// Gets or sets warnings generated during the build.
+ ///
+ public List Warnings { get; set; } = new();
+
+ ///
+ /// Creates a successful build result.
+ ///
+ /// Time taken for the build.
+ /// A successful build result.
+ public static BuildResult CreateSuccess(TimeSpan elapsed)
+ {
+ return new BuildResult(true, (IEnumerable?)null, elapsed);
+ }
+
+ ///
+ /// Creates a failed build result.
+ ///
+ /// The error message.
+ /// Time taken for the build.
+ /// A failed build result.
+ public static BuildResult CreateFailure(string error, TimeSpan elapsed)
+ {
+ return new BuildResult(false, error, elapsed);
+ }
+
+ ///
+ /// Creates a failed build result with multiple errors.
+ ///
+ /// The error messages.
+ /// Time taken for the build.
+ /// A failed build result.
+ public static BuildResult CreateFailure(IEnumerable errors, TimeSpan elapsed)
+ {
+ return new BuildResult(false, errors, elapsed);
+ }
+}
diff --git a/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildSetup.cs b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildSetup.cs
new file mode 100644
index 000000000..5470ee834
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildSetup.cs
@@ -0,0 +1,65 @@
+using System.Collections.Generic;
+
+namespace GenHub.Core.Models.Tools.ModBuilder;
+
+///
+/// Represents the build setup configuration.
+/// Placeholder for full implementation in Phase 1.
+///
+public sealed class BuildSetup
+{
+ ///
+ /// Gets or sets the build steps to execute.
+ ///
+ public BuildStep Step { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to enable verbose logging.
+ ///
+ public bool VerboseLogging { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to enable multi-processing.
+ ///
+ public bool MultiProcessing { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to print configuration.
+ ///
+ public bool PrintConfig { get; set; }
+
+ ///
+ /// Gets or sets the folders configuration.
+ ///
+ public Folders? Folders { get; set; }
+
+ ///
+ /// Gets or sets the bundles configuration.
+ ///
+ public Bundles? Bundles { get; set; }
+
+ ///
+ /// Gets or sets the runner configuration.
+ ///
+ public Runner? Runner { get; set; }
+
+ ///
+ /// Gets or sets the tools configuration.
+ ///
+ public Dictionary? Tools { get; set; }
+
+ ///
+ /// Gets or sets the absolute path to the game installation directory.
+ ///
+ public string? GameDirectory { get; set; }
+
+ ///
+ /// Gets or sets the game runner configuration for launching the game.
+ ///
+ public RunnerConfiguration? RunnerConfig { get; set; }
+
+ ///
+ /// Gets or sets the list of selected pack names to build or release. If null or empty, all enabled packs are processed.
+ ///
+ public List? SelectedPacks { get; set; }
+}
diff --git a/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildStep.cs b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildStep.cs
new file mode 100644
index 000000000..299aae648
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildStep.cs
@@ -0,0 +1,54 @@
+namespace GenHub.Core.Models.Tools.ModBuilder;
+
+///
+/// Represents build steps as flags.
+///
+[System.Flags]
+[System.Diagnostics.CodeAnalysis.SuppressMessage("SonarCloud", "S2342:Enumeration types should comply with a naming convention", Justification = "Preserved public domain model enum name")]
+public enum BuildStep
+{
+ ///
+ /// No build steps.
+ ///
+ None = 0,
+
+ ///
+ /// Execute pre-build tasks.
+ ///
+ PreBuild = 1 << 0,
+
+ ///
+ /// Clean build artifacts.
+ ///
+ Clean = 1 << 1,
+
+ ///
+ /// Execute main build process.
+ ///
+ Build = 1 << 2,
+
+ ///
+ /// Execute post-build tasks.
+ ///
+ PostBuild = 1 << 3,
+
+ ///
+ /// Create release packages.
+ ///
+ Release = 1 << 4,
+
+ ///
+ /// Install to game directory.
+ ///
+ Install = 1 << 5,
+
+ ///
+ /// Run the game.
+ ///
+ Run = 1 << 6,
+
+ ///
+ /// Uninstall from game directory.
+ ///
+ Uninstall = 1 << 7,
+}
diff --git a/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildStructure.cs b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildStructure.cs
new file mode 100644
index 000000000..f3ddd03b8
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BuildStructure.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+
+namespace GenHub.Core.Models.Tools.ModBuilder;
+
+///
+/// Represents the parsed build structure containing all build stages and file mappings.
+/// This structure is cached to avoid re-parsing configurations on every build.
+///
+public sealed class BuildStructure
+{
+ ///
+ /// Gets the project this build structure belongs to.
+ ///
+ public required ModBuilderProject Project { get; init; }
+
+ ///
+ /// Gets the build configuration.
+ ///
+ public required BuildConfiguration Configuration { get; init; }
+
+ ///
+ /// Gets the build setup derived from configuration.
+ ///
+ public required BuildSetup Setup { get; init; }
+
+ ///
+ /// Gets the file mappings for each build stage.
+ /// Key: BuildIndex, Value: List of source file paths to process.
+ ///
+ public Dictionary> StageFiles { get; init; } = new();
+
+ ///
+ /// Gets the bundle items indexed by name.
+ ///
+ public Dictionary BundleItems { get; init; } = new();
+
+ ///
+ /// Gets the bundle packs indexed by name.
+ ///
+ public Dictionary BundlePacks { get; init; } = new();
+
+ ///
+ /// Gets the timestamp when this structure was created.
+ ///
+ public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
+}
diff --git a/GenHub/GenHub.Core/Models/Tools/ModBuilder/BundleEvent.cs b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BundleEvent.cs
new file mode 100644
index 000000000..d062dd7e8
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BundleEvent.cs
@@ -0,0 +1,51 @@
+using System.Text.Json.Serialization;
+
+namespace GenHub.Core.Models.Tools.ModBuilder;
+
+///
+/// Represents an event callback configuration for the build system.
+///
+public class BundleEvent
+{
+ ///
+ /// Gets or sets the type of event this callback handles.
+ ///
+ [JsonPropertyName("type")]
+ public BundleEventType Type { get; set; }
+
+ ///
+ /// Gets or sets the absolute path to the script file containing the callback.
+ ///
+ [JsonPropertyName("absScript")]
+ public string AbsScript { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the name of the function to call in the script.
+ ///
+ [JsonPropertyName("funcName")]
+ public string FuncName { get; set; } = "OnEvent";
+
+ ///
+ /// Gets or sets additional keyword arguments to pass to the callback function.
+ ///
+ [JsonPropertyName("kwargs")]
+ public Dictionary Kwargs { get; set; } = new();
+
+ ///
+ /// Gets the directory containing the script file.
+ ///
+ /// The directory path containing the script file.
+ public string GetScriptDir()
+ {
+ return Path.GetDirectoryName(AbsScript) ?? string.Empty;
+ }
+
+ ///
+ /// Gets the script file name without extension.
+ ///
+ /// The script file name without extension.
+ public string GetScriptName()
+ {
+ return Path.GetFileNameWithoutExtension(AbsScript);
+ }
+}
diff --git a/GenHub/GenHub.Core/Models/Tools/ModBuilder/BundleEventArgs.cs b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BundleEventArgs.cs
new file mode 100644
index 000000000..308b1ae00
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BundleEventArgs.cs
@@ -0,0 +1,34 @@
+using System;
+
+namespace GenHub.Core.Models.Tools.ModBuilder;
+
+///
+/// Event arguments for bundle events.
+///
+public class BundleEventArgs : EventArgs
+{
+ ///
+ /// Gets or sets the event type.
+ ///
+ public required BundleEventType EventType { get; set; }
+
+ ///
+ /// Gets or sets the bundle item name (if applicable).
+ ///
+ public string? BundleItemName { get; set; }
+
+ ///
+ /// Gets or sets the bundle pack name (if applicable).
+ ///
+ public string? BundlePackName { get; set; }
+
+ ///
+ /// Gets or sets the build index (stage).
+ ///
+ public BuildIndex? BuildIndex { get; set; }
+
+ ///
+ /// Gets or sets additional event data.
+ ///
+ public Dictionary Data { get; set; } = new();
+}
diff --git a/GenHub/GenHub.Core/Models/Tools/ModBuilder/BundleEventType.cs b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BundleEventType.cs
new file mode 100644
index 000000000..74b21f387
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BundleEventType.cs
@@ -0,0 +1,92 @@
+namespace GenHub.Core.Models.Tools.ModBuilder;
+
+///
+/// Represents the 17 event types across the build lifecycle.
+///
+public enum BundleEventType
+{
+ ///
+ /// Fired before the build process starts.
+ ///
+ OnPreBuild = 0,
+
+ ///
+ /// Fired during the build process.
+ ///
+ OnBuild = 1,
+
+ ///
+ /// Fired after the build process completes.
+ ///
+ OnPostBuild = 2,
+
+ ///
+ /// Fired during the release process.
+ ///
+ OnRelease = 3,
+
+ ///
+ /// Fired during the install process.
+ ///
+ OnInstall = 4,
+
+ ///
+ /// Fired when the game is run.
+ ///
+ OnRun = 5,
+
+ ///
+ /// Fired during the uninstall process.
+ ///
+ OnUninstall = 6,
+
+ ///
+ /// Fired at the start of RawBundleItem stage.
+ ///
+ OnStartBuildRawBundleItem = 7,
+
+ ///
+ /// Fired at the finish of RawBundleItem stage.
+ ///
+ OnFinishBuildRawBundleItem = 8,
+
+ ///
+ /// Fired at the start of BigBundleItem stage.
+ ///
+ OnStartBuildBigBundleItem = 9,
+
+ ///
+ /// Fired at the finish of BigBundleItem stage.
+ ///
+ OnFinishBuildBigBundleItem = 10,
+
+ ///
+ /// Fired at the start of RawBundlePack stage.
+ ///
+ OnStartBuildRawBundlePack = 11,
+
+ ///
+ /// Fired at the finish of RawBundlePack stage.
+ ///
+ OnFinishBuildRawBundlePack = 12,
+
+ ///
+ /// Fired at the start of ReleaseBundlePack stage.
+ ///
+ OnStartBuildReleaseBundlePack = 13,
+
+ ///
+ /// Fired at the finish of ReleaseBundlePack stage.
+ ///
+ OnFinishBuildReleaseBundlePack = 14,
+
+ ///
+ /// Fired at the start of InstallBundlePack stage.
+ ///
+ OnStartBuildInstallBundlePack = 15,
+
+ ///
+ /// Fired at the finish of InstallBundlePack stage.
+ ///
+ OnFinishBuildInstallBundlePack = 16,
+}
diff --git a/GenHub/GenHub.Core/Models/Tools/ModBuilder/BundleFile.cs b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BundleFile.cs
new file mode 100644
index 000000000..7fb86b66f
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BundleFile.cs
@@ -0,0 +1,65 @@
+using System.Text.Json.Serialization;
+
+namespace GenHub.Core.Models.Tools.ModBuilder;
+
+///
+/// Represents a source-to-target file mapping with conversion parameters for the build system.
+///
+public class BundleFile
+{
+ ///
+ /// Gets or sets the absolute path to the source file's parent directory.
+ ///
+ [JsonPropertyName("absSourceParent")]
+ public string AbsSourceParent { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the absolute path to the source file.
+ ///
+ [JsonPropertyName("absSourceFile")]
+ public string AbsSourceFile { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the relative path for the target file.
+ ///
+ [JsonPropertyName("relTargetFile")]
+ public string RelTargetFile { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the conversion parameters for this file.
+ ///
+ [JsonPropertyName("params")]
+ public Dictionary? Params { get; set; }
+
+ ///
+ /// Gets or sets the list of delimiter marker pairs to exclude from text files.
+ ///
+ [JsonPropertyName("excludeMarkersList")]
+ public List>? ExcludeMarkersList { get; set; }
+
+ ///
+ /// Gets or sets the file hash registry definition for change detection.
+ ///
+ [JsonPropertyName("registry")]
+ public BundleRegistryDefinition? RegistryDef { get; set; }
+
+ ///
+ /// Gets the relative source file path by removing the parent directory prefix.
+ ///
+ /// The relative source file path.
+ public string GetRelSourceFile()
+ {
+ if (string.IsNullOrEmpty(AbsSourceParent) || string.IsNullOrEmpty(AbsSourceFile))
+ return string.Empty;
+
+ var normalized = Path.GetFullPath(AbsSourceFile);
+ var parent = Path.GetFullPath(AbsSourceParent);
+
+ if (normalized.StartsWith(parent, StringComparison.OrdinalIgnoreCase))
+ {
+ return normalized.Substring(parent.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+ }
+
+ return AbsSourceFile;
+ }
+}
diff --git a/GenHub/GenHub.Core/Models/Tools/ModBuilder/BundleItem.cs b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BundleItem.cs
new file mode 100644
index 000000000..58e80bd55
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BundleItem.cs
@@ -0,0 +1,66 @@
+using System.Text.Json.Serialization;
+
+namespace GenHub.Core.Models.Tools.ModBuilder;
+
+///
+/// Represents a bundle item containing file mappings and build configuration.
+///
+public class BundleItem
+{
+ ///
+ /// Gets or sets the unique name of this bundle item.
+ ///
+ [JsonPropertyName("name")]
+ public required string Name { get; set; }
+
+ ///
+ /// Gets or sets the list of files to be processed in this bundle item.
+ ///
+ [JsonPropertyName("files")]
+ public List Files { get; set; } = new();
+
+ ///
+ /// Gets or sets the prefix to add to the bundle item name.
+ ///
+ [JsonPropertyName("namePrefix")]
+ public string NamePrefix { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the suffix to add to the bundle item name.
+ ///
+ [JsonPropertyName("nameSuffix")]
+ public string NameSuffix { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets a value indicating whether this bundle should be packaged as a .big archive.
+ ///
+ [JsonPropertyName("isBig")]
+ public bool IsBig { get; set; } = true;
+
+ ///
+ /// Gets or sets the suffix to add to the .big archive name.
+ ///
+ [JsonPropertyName("bigSuffix")]
+ public string BigSuffix { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the game language to set on installation.
+ ///
+ [JsonPropertyName("setGameLanguageOnInstall")]
+ public string SetGameLanguageOnInstall { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the event callbacks for this bundle item.
+ ///
+ [JsonPropertyName("events")]
+ public Dictionary Events { get; set; } = new();
+
+ ///
+ /// Gets the full name of this bundle item including prefix and suffix.
+ ///
+ /// The full name of the bundle item.
+ public string GetFullName()
+ {
+ return $"{NamePrefix}{Name}{NameSuffix}";
+ }
+}
diff --git a/GenHub/GenHub.Core/Models/Tools/ModBuilder/BundlePack.cs b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BundlePack.cs
new file mode 100644
index 000000000..62c7c9388
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BundlePack.cs
@@ -0,0 +1,82 @@
+using System.Text.Json.Serialization;
+
+namespace GenHub.Core.Models.Tools.ModBuilder;
+
+///
+/// Represents a grouping of bundle items for distribution and installation.
+///
+public class BundlePack
+{
+ ///
+ /// Gets or sets the unique name of this bundle pack.
+ ///
+ [JsonPropertyName("name")]
+ public required string Name { get; set; }
+
+ ///
+ /// Gets or sets the list of bundle item names included in this pack.
+ ///
+ [JsonPropertyName("itemNames")]
+ public List ItemNames { get; set; } = new();
+
+ ///
+ /// Sets the alias property for itemNames to support "items" JSON key.
+ ///
+ [JsonPropertyName("items")]
+ public List? Items
+ {
+ private get => ItemNames;
+ set
+ {
+ if (value != null)
+ {
+ ItemNames = value;
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the prefix to add to the bundle pack name.
+ ///
+ [JsonPropertyName("namePrefix")]
+ public string NamePrefix { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the suffix to add to the bundle pack name.
+ ///
+ [JsonPropertyName("nameSuffix")]
+ public string NameSuffix { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets a value indicating whether this pack should be built.
+ ///
+ [JsonPropertyName("allowBuild")]
+ public bool AllowBuild { get; set; } = false;
+
+ ///
+ /// Gets or sets a value indicating whether this pack can be installed.
+ ///
+ [JsonPropertyName("allowInstall")]
+ public bool AllowInstall { get; set; } = false;
+
+ ///
+ /// Gets or sets the game language to set on installation.
+ ///
+ [JsonPropertyName("setGameLanguageOnInstall")]
+ public string SetGameLanguageOnInstall { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the event callbacks for this bundle pack.
+ ///
+ [JsonPropertyName("events")]
+ public Dictionary Events { get; set; } = new();
+
+ ///
+ /// Gets the full name of this bundle pack including prefix and suffix.
+ ///
+ /// The full name of the bundle pack.
+ public string GetFullName()
+ {
+ return $"{NamePrefix}{Name}{NameSuffix}";
+ }
+}
diff --git a/GenHub/GenHub.Core/Models/Tools/ModBuilder/BundleRegistryDefinition.cs b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BundleRegistryDefinition.cs
new file mode 100644
index 000000000..5e2f1772a
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Tools/ModBuilder/BundleRegistryDefinition.cs
@@ -0,0 +1,65 @@
+using System.Text;
+using System.Text.Json.Serialization;
+
+namespace GenHub.Core.Models.Tools.ModBuilder;
+
+///
+/// Represents a file hash registry definition for change detection optimization.
+///
+public class BundleRegistryDefinition
+{
+ ///
+ /// Gets or sets the list of registry file paths.
+ ///
+ [JsonPropertyName("paths")]
+ public List Paths { get; set; } = new();
+
+ ///
+ /// Gets or sets the CRC32 checksum of all registry paths combined.
+ ///
+ [JsonPropertyName("crc32")]
+ public uint Crc32 { get; set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public BundleRegistryDefinition()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class with paths.
+ ///
+ /// The registry file paths.
+ public BundleRegistryDefinition(List paths)
+ {
+ Paths = paths ?? new List();
+ if (Paths.Count > 0)
+ {
+ Crc32 = CalculateCrc32();
+ }
+ }
+
+ ///
+ /// Calculates the CRC32 checksum of all paths combined.
+ ///
+ /// The CRC32 checksum value.
+ private uint CalculateCrc32()
+ {
+ var pathsStr = string.Join(string.Empty, Paths);
+ var pathsBytes = Encoding.UTF8.GetBytes(pathsStr);
+
+ // Simple CRC32 implementation
+ uint crc = 0xFFFFFFFF;
+ foreach (var b in pathsBytes)
+ {
+ crc ^= b;
+ for (int i = 0; i < 8; i++)
+ {
+ crc = (crc & 1) != 0 ? (crc >> 1) ^ 0xEDB88320 : crc >> 1;
+ }
+ }
+
+ return ~crc;
+ }
+}
diff --git a/GenHub/GenHub.Core/Models/Tools/ModBuilder/Bundles.cs b/GenHub/GenHub.Core/Models/Tools/ModBuilder/Bundles.cs
new file mode 100644
index 000000000..dcf26b7c6
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Tools/ModBuilder/Bundles.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+
+namespace GenHub.Core.Models.Tools.ModBuilder;
+
+///
+/// Placeholder for Bundles configuration.
+///
+public sealed class Bundles
+{
+ ///
+ /// Gets or sets the list of bundle items.
+ ///
+ public List? Items { get; set; }
+
+ ///
+ /// Gets or sets the list of bundle packs.
+ ///
+ public List? Packs { get; set; }
+}
diff --git a/GenHub/GenHub.Core/Models/Tools/ModBuilder/Converters/BundlePackListConverter.cs b/GenHub/GenHub.Core/Models/Tools/ModBuilder/Converters/BundlePackListConverter.cs
new file mode 100644
index 000000000..97827383f
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Tools/ModBuilder/Converters/BundlePackListConverter.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace GenHub.Core.Models.Tools.ModBuilder.Converters;
+
+///
+/// Handles deserialization of BundlePack lists from both JSON arrays ([]) and objects/dictionaries ({}) format.
+///
+[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CS-R1138:Inappropriate ordering of parameters", Justification = "Overridden from System.Text.Json.Serialization.JsonConverter")]
+public sealed class BundlePackListConverter : JsonConverter>
+{
+ ///
+ // skipcq: CS-R1138
+ public override List? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) // skipcq: CS-R1138
+ {
+ return reader.TokenType switch
+ {
+ JsonTokenType.Null => [],
+ JsonTokenType.StartArray => ReadArray(ref reader, options),
+ JsonTokenType.StartObject => ReadObject(ref reader, options),
+ _ => throw new JsonException($"Unexpected token type {reader.TokenType} for BundlePack list")
+ };
+ }
+
+ private static List ReadArray(ref Utf8JsonReader reader, JsonSerializerOptions options)
+ {
+ var list = new List();
+ while (reader.Read())
+ {
+ if (reader.TokenType == JsonTokenType.EndArray)
+ {
+ return list;
+ }
+
+ var item = JsonSerializer.Deserialize(ref reader, options);
+ if (item != null)
+ {
+ list.Add(item);
+ }
+ }
+
+ return list;
+ }
+
+ private static List ReadObject(ref Utf8JsonReader reader, JsonSerializerOptions options)
+ {
+ var list = new List();
+ using var doc = JsonDocument.ParseValue(ref reader);
+
+ foreach (var prop in doc.RootElement.EnumerateObject())
+ {
+ if (prop.Value.ValueKind != JsonValueKind.Object)
+ {
+ continue;
+ }
+
+ var pack = JsonSerializer.Deserialize(prop.Value.GetRawText(), options);
+ if (pack != null)
+ {
+ if (string.IsNullOrEmpty(pack.Name))
+ {
+ pack.Name = prop.Name;
+ }
+
+ list.Add(pack);
+ }
+ }
+
+ return list;
+ }
+
+ ///
+ public override void Write(Utf8JsonWriter writer, List value, JsonSerializerOptions options)
+ {
+ JsonSerializer.Serialize(writer, value, options);
+ }
+}
diff --git a/GenHub/GenHub.Core/Models/Tools/ModBuilder/FolderConfiguration.cs b/GenHub/GenHub.Core/Models/Tools/ModBuilder/FolderConfiguration.cs
new file mode 100644
index 000000000..cdb698785
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Tools/ModBuilder/FolderConfiguration.cs
@@ -0,0 +1,27 @@
+using System.Text.Json.Serialization;
+
+namespace GenHub.Core.Models.Tools.ModBuilder;
+
+///
+/// Represents folder paths for build outputs.
+///
+public class FolderConfiguration
+{
+ ///
+ /// Gets or sets the absolute path to the build directory.
+ ///
+ [JsonPropertyName("absBuildDir")]
+ public string AbsBuildDir { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the absolute path to the release directory.
+ ///
+ [JsonPropertyName("absReleaseDir")]
+ public string AbsReleaseDir { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the absolute path to the game installation directory.
+ ///
+ [JsonPropertyName("absGameDir")]
+ public string AbsGameDir { get; set; } = string.Empty;
+}
diff --git a/GenHub/GenHub.Core/Models/Tools/ModBuilder/Folders.cs b/GenHub/GenHub.Core/Models/Tools/ModBuilder/Folders.cs
new file mode 100644
index 000000000..45e058eb2
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Tools/ModBuilder/Folders.cs
@@ -0,0 +1,22 @@
+namespace GenHub.Core.Models.Tools.ModBuilder;
+
+///
+/// Placeholder for Folders configuration.
+///
+public sealed class Folders
+{
+ ///
+ /// Gets or sets the absolute path to the build directory.
+ ///
+ public string? AbsBuildDir { get; set; }
+
+ ///
+ /// Gets or sets the absolute path to the release directory.
+ ///
+ public string? AbsReleaseDir { get; set; }
+
+ ///
+ /// Gets or sets the absolute path to the game installation directory.
+ ///
+ public string? AbsGameDir { get; set; }
+}
diff --git a/GenHub/GenHub.Core/Models/Tools/ModBuilder/ModBuilderProject.cs b/GenHub/GenHub.Core/Models/Tools/ModBuilder/ModBuilderProject.cs
new file mode 100644
index 000000000..ce1694005
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Tools/ModBuilder/ModBuilderProject.cs
@@ -0,0 +1,115 @@
+using System.Text.Json.Serialization;
+
+namespace GenHub.Core.Models.Tools.ModBuilder;
+
+///
+/// Represents a ModBuilder project container with metadata and configuration.
+///
+public class ModBuilderProject
+{
+ ///
+ /// Gets or sets the project name.
+ ///
+ [JsonPropertyName("name")]
+ public required string Name { get; set; }
+
+ ///
+ /// Gets or sets the project version.
+ ///
+ [JsonPropertyName("version")]
+ public string Version { get; set; } = "1.0.0";
+
+ ///
+ /// Gets or sets the project description.
+ ///
+ [JsonPropertyName("description")]
+ public string Description { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the project author.
+ ///
+ [JsonPropertyName("author")]
+ public string Author { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the absolute path to the project directory.
+ ///
+ [JsonPropertyName("projectDir")]
+ public string ProjectDir { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the absolute path to the game installation.
+ ///
+ [JsonPropertyName("gameDir")]
+ public string GameDir { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the game installation ID (for linking to game profiles).
+ ///
+ [JsonPropertyName("gameInstallationId")]
+ public string? GameInstallationId { get; set; }
+
+ ///
+ /// Gets or sets the project directory structure configuration.
+ ///
+ [JsonPropertyName("directories")]
+ public ProjectDirectories Directories { get; set; } = new();
+
+ ///
+ /// Gets or sets the list of configuration file paths to load.
+ ///
+ [JsonPropertyName("configFiles")]
+ public List ConfigFiles { get; set; } = new();
+
+ ///
+ /// Gets or sets the bundle configuration file paths.
+ ///
+ [JsonPropertyName("bundleConfigs")]
+ public List BundleConfigs { get; set; } = new();
+
+ ///
+ /// Gets or sets the list of bundle packs in this project.
+ ///
+ [JsonPropertyName("bundlePacks")]
+ public List BundlePacks { get; set; } = new();
+
+ ///
+ /// Gets or sets the build configuration.
+ ///
+ [JsonIgnore]
+ public BuildConfiguration? Configuration { get; set; }
+
+ ///
+ /// Gets or sets the date the project was created.
+ ///
+ [JsonPropertyName("createdAt")]
+ public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+
+ ///
+ /// Gets or sets the date the project was last modified.
+ ///
+ [JsonPropertyName("modifiedAt")]
+ public DateTime ModifiedAt { get; set; } = DateTime.UtcNow;
+
+ ///
+ /// Gets or sets the date the project was last modified (alias for compatibility).
+ ///
+ [JsonPropertyName("lastModified")]
+ public DateTime LastModified
+ {
+ get => ModifiedAt;
+ set => ModifiedAt = value;
+ }
+
+ ///
+ /// Gets or sets the date of the last successful build.
+ ///
+ [JsonPropertyName("lastBuild")]
+ public DateTime? LastBuild { get; set; }
+
+ ///
+ /// Gets or sets additional project metadata.
+ ///
+ [JsonPropertyName("metadata")]
+ public Dictionary Metadata { get; set; } = new();
+}
diff --git a/GenHub/GenHub.Core/Models/Tools/ModBuilder/ProjectDirectories.cs b/GenHub/GenHub.Core/Models/Tools/ModBuilder/ProjectDirectories.cs
new file mode 100644
index 000000000..c59e97cca
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Tools/ModBuilder/ProjectDirectories.cs
@@ -0,0 +1,53 @@
+using System.Text.Json.Serialization;
+
+namespace GenHub.Core.Models.Tools.ModBuilder;
+
+///
+/// Represents the directory structure for a ModBuilder project.
+///
+public class ProjectDirectories
+{
+ ///
+ /// Gets or sets the relative path to the configs directory.
+ ///
+ [JsonPropertyName("configs")]
+ public string Configs { get; set; } = "Configs";
+
+ ///
+ /// Gets or sets the relative path to the configs directory (alias for compatibility).
+ ///
+ [JsonPropertyName("config")]
+ public string Config
+ {
+ get => Configs;
+ set => Configs = value;
+ }
+
+ ///
+ /// Gets or sets the relative path to the game files edited directory.
+ ///
+ [JsonPropertyName("gameFilesEdited")]
+ public string GameFilesEdited { get; set; } = "GameFilesEdited";
+
+ ///
+ /// Gets or sets the relative path to the build directory.
+ ///
+ [JsonPropertyName("build")]
+ public string Build { get; set; } = ".Build";
+
+ ///
+ /// Gets or sets the relative path to the release directory.
+ ///
+ [JsonPropertyName("release")]
+ public string Release { get; set; } = ".Release";
+
+ ///
+ /// Gets or sets the relative path to the release directory (alias for compatibility).
+ ///
+ [JsonPropertyName("output")]
+ public string Output
+ {
+ get => Release;
+ set => Release = value;
+ }
+}
diff --git a/GenHub/GenHub.Core/Models/Tools/ModBuilder/ProjectTemplate.cs b/GenHub/GenHub.Core/Models/Tools/ModBuilder/ProjectTemplate.cs
new file mode 100644
index 000000000..3d9800884
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Tools/ModBuilder/ProjectTemplate.cs
@@ -0,0 +1,53 @@
+namespace GenHub.Core.Models.Tools.ModBuilder;
+
+///
+/// Represents a project template for creating new ModBuilder projects.
+///
+public class ProjectTemplate
+{
+ ///
+ /// Gets the empty project template.
+ ///
+ public static ProjectTemplate Empty => new()
+ {
+ Name = "Empty",
+ Description = "Empty project with no default configurations",
+ CreateSampleFiles = false,
+ };
+
+ ///
+ /// Gets the basic mod template.
+ ///
+ public static ProjectTemplate BasicMod => new()
+ {
+ Name = "Basic Mod",
+ Description = "Basic mod project with standard configurations",
+ DefaultBundleConfigs = new List
+ {
+ "Configs/ModBundleItems.json",
+ "Configs/ModBundlePacks.json",
+ "Configs/ModFolders.json",
+ },
+ CreateSampleFiles = true,
+ };
+
+ ///
+ /// Gets or sets the template name.
+ ///
+ public required string Name { get; set; }
+
+ ///
+ /// Gets or sets the template description.
+ ///
+ public string? Description { get; set; }
+
+ ///
+ /// Gets or sets the default bundle configurations to include.
+ ///
+ public List DefaultBundleConfigs { get; set; } = new();
+
+ ///
+ /// Gets or sets a value indicating whether to create sample files.
+ ///
+ public bool CreateSampleFiles { get; set; }
+}
diff --git a/GenHub/GenHub.Core/Models/Tools/ModBuilder/PythonConfigModels.cs b/GenHub/GenHub.Core/Models/Tools/ModBuilder/PythonConfigModels.cs
new file mode 100644
index 000000000..1d5b1bae4
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Tools/ModBuilder/PythonConfigModels.cs
@@ -0,0 +1,270 @@
+using System.Text.Json.Serialization;
+
+namespace GenHub.Core.Models.Tools.ModBuilder;
+
+///
+/// Root wrapper for Python ModBuilder configuration files.
+///
+public sealed class PythonConfigRoot
+{
+ [JsonPropertyName("bundles")]
+ public PythonBundlesConfig? Bundles { get; set; }
+}
+
+///
+/// Python bundles configuration containing items and packs.
+///
+public sealed class PythonBundlesConfig
+{
+ [JsonPropertyName("version")]
+ public int Version { get; set; }
+
+ [JsonPropertyName("itemsPrefix")]
+ public string ItemsPrefix { get; set; } = string.Empty;
+
+ [JsonPropertyName("itemsSuffix")]
+ public string ItemsSuffix { get; set; } = string.Empty;
+
+ [JsonPropertyName("packsPrefix")]
+ public string PacksPrefix { get; set; } = string.Empty;
+
+ [JsonPropertyName("packsSuffix")]
+ public string PacksSuffix { get; set; } = string.Empty;
+
+ [JsonPropertyName("items")]
+ public List? Items { get; set; }
+
+ [JsonPropertyName("packs")]
+ public List? Packs { get; set; }
+}
+
+///
+/// Python bundle item configuration.
+///
+public sealed class PythonBundleItem
+{
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+
+ [JsonPropertyName("namePrefix")]
+ public string NamePrefix { get; set; } = string.Empty;
+
+ [JsonPropertyName("nameSuffix")]
+ public string NameSuffix { get; set; } = string.Empty;
+
+ [JsonPropertyName("big")]
+ public bool Big { get; set; } = true;
+
+ [JsonPropertyName("bigSuffix")]
+ public string BigSuffix { get; set; } = string.Empty;
+
+ [JsonPropertyName("setGameLanguageOnInstall")]
+ public string SetGameLanguageOnInstall { get; set; } = string.Empty;
+
+ [JsonPropertyName("files")]
+ public List? Files { get; set; }
+
+ [JsonPropertyName("onPreBuild")]
+ public PythonBundleEvent? OnPreBuild { get; set; }
+
+ [JsonPropertyName("onBuild")]
+ public PythonBundleEvent? OnBuild { get; set; }
+
+ [JsonPropertyName("onPostBuild")]
+ public PythonBundleEvent? OnPostBuild { get; set; }
+}
+
+///
+/// Python bundle pack configuration.
+///
+public sealed class PythonBundlePack
+{
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+
+ [JsonPropertyName("namePrefix")]
+ public string NamePrefix { get; set; } = string.Empty;
+
+ [JsonPropertyName("nameSuffix")]
+ public string NameSuffix { get; set; } = string.Empty;
+
+ [JsonPropertyName("allowBuild")]
+ public bool AllowBuild { get; set; }
+
+ [JsonPropertyName("allowInstall")]
+ public bool AllowInstall { get; set; }
+
+ [JsonPropertyName("setGameLanguageOnInstall")]
+ public string SetGameLanguageOnInstall { get; set; } = string.Empty;
+
+ [JsonPropertyName("itemNames")]
+ public List? ItemNames { get; set; }
+
+ [JsonPropertyName("onPreBuild")]
+ public PythonBundleEvent? OnPreBuild { get; set; }
+
+ [JsonPropertyName("onRelease")]
+ public PythonBundleEvent? OnRelease { get; set; }
+
+ [JsonPropertyName("onInstall")]
+ public PythonBundleEvent? OnInstall { get; set; }
+
+ [JsonPropertyName("onRun")]
+ public PythonBundleEvent? OnRun { get; set; }
+
+ [JsonPropertyName("onUninstall")]
+ public PythonBundleEvent? OnUninstall { get; set; }
+}
+
+///
+/// Python file group with source/target mappings.
+///
+public sealed class PythonBundleFileGroup
+{
+ [JsonPropertyName("sourceParent")]
+ public string SourceParent { get; set; } = string.Empty;
+
+ [JsonPropertyName("source")]
+ public string? Source { get; set; }
+
+ [JsonPropertyName("target")]
+ public string? Target { get; set; }
+
+ [JsonPropertyName("sourceList")]
+ public List? SourceList { get; set; }
+
+ [JsonPropertyName("sourceTargetList")]
+ public List? SourceTargetList { get; set; }
+
+ [JsonPropertyName("registryList")]
+ public List? RegistryList { get; set; }
+
+ [JsonPropertyName("params")]
+ public Dictionary? Params { get; set; }
+
+ [JsonPropertyName("excludeMarkersList")]
+ public List>? ExcludeMarkersList { get; set; }
+}
+
+///
+/// Python source-target pair for file mappings.
+///
+public sealed class PythonSourceTargetPair
+{
+ [JsonPropertyName("source")]
+ public string Source { get; set; } = string.Empty;
+
+ [JsonPropertyName("target")]
+ public string Target { get; set; } = string.Empty;
+}
+
+///
+/// Python bundle event configuration.
+///
+public sealed class PythonBundleEvent
+{
+ [JsonPropertyName("script")]
+ public string Script { get; set; } = string.Empty;
+
+ [JsonPropertyName("args")]
+ public string? Args { get; set; }
+}
+
+///
+/// ModJsonFiles.json master configuration list.
+///
+public sealed class PythonModJsonFilesConfig
+{
+ [JsonPropertyName("build")]
+ public PythonModJsonFilesBuild? Build { get; set; }
+}
+
+public sealed class PythonModJsonFilesBuild
+{
+ [JsonPropertyName("version")]
+ public int Version { get; set; }
+
+ [JsonPropertyName("files")]
+ public List? Files { get; set; }
+}
+
+///
+/// ModFolders.json folders configuration.
+///
+public sealed class PythonModFoldersConfig
+{
+ [JsonPropertyName("folders")]
+ public PythonModFoldersData? Folders { get; set; }
+}
+
+public sealed class PythonModFoldersData
+{
+ [JsonPropertyName("version")]
+ public int Version { get; set; }
+
+ [JsonPropertyName("buildDir")]
+ public string? BuildDir { get; set; }
+
+ [JsonPropertyName("releaseDir")]
+ public string? ReleaseDir { get; set; }
+
+ [JsonPropertyName("gameDir")]
+ public string? GameDir { get; set; }
+}
+
+///
+/// Simplified configuration format used in sample projects.
+///
+public sealed class SimplifiedConfigRoot
+{
+ [JsonPropertyName("BundleItems")]
+ public List? BundleItems { get; set; }
+
+ [JsonPropertyName("BundlePacks")]
+ public List? BundlePacks { get; set; }
+}
+
+///
+/// Simplified bundle item with wildcard patterns.
+///
+public sealed class SimplifiedBundleItem
+{
+ [JsonPropertyName("Name")]
+ public string? Name { get; set; }
+
+ [JsonPropertyName("SourceFiles")]
+ public List? SourceFiles { get; set; }
+
+ [JsonPropertyName("OutputFormat")]
+ public string? OutputFormat { get; set; }
+
+ [JsonPropertyName("Compression")]
+ public string? Compression { get; set; }
+
+ [JsonPropertyName("GenerateMipmaps")]
+ public bool GenerateMipmaps { get; set; }
+}
+
+///
+/// Simplified bundle pack format used in sample projects.
+///
+public sealed class SimplifiedBundlePack
+{
+ [JsonPropertyName("Name")]
+ public string? Name { get; set; }
+
+ [JsonPropertyName("Items")]
+ public List? Items { get; set; }
+
+ [JsonPropertyName("ItemNames")]
+ public List? ItemNames { get; set; }
+
+ [JsonPropertyName("OutputFile")]
+ public string? OutputFile { get; set; }
+
+ [JsonPropertyName("AllowBuild")]
+ public bool? AllowBuild { get; set; }
+
+ [JsonPropertyName("AllowInstall")]
+ public bool? AllowInstall { get; set; }
+}
diff --git a/GenHub/GenHub.Core/Models/Tools/ModBuilder/Runner.cs b/GenHub/GenHub.Core/Models/Tools/ModBuilder/Runner.cs
new file mode 100644
index 000000000..9bcd430aa
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Tools/ModBuilder/Runner.cs
@@ -0,0 +1,22 @@
+namespace GenHub.Core.Models.Tools.ModBuilder;
+
+///
+/// Represents the runner configuration for launching the game.
+///
+public sealed class Runner
+{
+ ///
+ /// Gets or sets the absolute path to the game executable.
+ ///
+ public string? AbsExe { get; set; }
+
+ ///
+ /// Gets or sets the command-line arguments for the game.
+ ///
+ public string? Args { get; set; }
+
+ ///
+ /// Gets or sets the working directory for the game process.
+ ///
+ public string? WorkingDir { get; set; }
+}
diff --git a/GenHub/GenHub.Core/Models/Tools/ModBuilder/RunnerConfiguration.cs b/GenHub/GenHub.Core/Models/Tools/ModBuilder/RunnerConfiguration.cs
new file mode 100644
index 000000000..d770090b0
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Tools/ModBuilder/RunnerConfiguration.cs
@@ -0,0 +1,33 @@
+using System.Text.Json.Serialization;
+
+namespace GenHub.Core.Models.Tools.ModBuilder;
+
+///
+/// Represents game runner configuration.
+///
+public class RunnerConfiguration
+{
+ ///
+ /// Gets or sets the absolute path to the game executable.
+ ///
+ [JsonPropertyName("absExe")]
+ public string AbsExe { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the command-line arguments for the game.
+ ///
+ [JsonPropertyName("args")]
+ public string Args { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the working directory for the game process.
+ ///
+ [JsonPropertyName("workingDir")]
+ public string WorkingDir { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the path to the mod folder for native game -mod command line argument.
+ ///
+ [JsonPropertyName("modFolder")]
+ public string ModFolder { get; set; } = string.Empty;
+}
diff --git a/GenHub/GenHub.Core/Models/Tools/ModBuilder/ToolConfiguration.cs b/GenHub/GenHub.Core/Models/Tools/ModBuilder/ToolConfiguration.cs
new file mode 100644
index 000000000..3a2647966
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Tools/ModBuilder/ToolConfiguration.cs
@@ -0,0 +1,27 @@
+using System.Text.Json.Serialization;
+
+namespace GenHub.Core.Models.Tools.ModBuilder;
+
+///
+/// Represents external tool configuration.
+///
+public class ToolConfiguration
+{
+ ///
+ /// Gets or sets the absolute path to the tool executable.
+ ///
+ [JsonPropertyName("absExe")]
+ public string AbsExe { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the SHA256 hash for tool verification.
+ ///
+ [JsonPropertyName("sha256")]
+ public string Sha256 { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the tool version.
+ ///
+ [JsonPropertyName("version")]
+ public string Version { get; set; } = string.Empty;
+}
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Constants/IoConstantsTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Constants/IoConstantsTests.cs
index 742ebd910..a7b4778c3 100644
--- a/GenHub/GenHub.Tests/GenHub.Tests.Core/Constants/IoConstantsTests.cs
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Constants/IoConstantsTests.cs
@@ -14,7 +14,7 @@ public class IoConstantsTests
public void IoConstants_ShouldHaveExpectedValues()
{
// Arrange & Act & Assert
- Assert.Equal(4096, IoConstants.DefaultFileBufferSize);
+ Assert.Equal(65536, IoConstants.DefaultFileBufferSize);
}
///
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/GameProcessManagerTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/GameProcessManagerTests.cs
index 443b0f5cf..b7e44502c 100644
--- a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/GameProcessManagerTests.cs
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/GameProcessManagerTests.cs
@@ -538,7 +538,9 @@ public static LauncherHarness Create(
// Batch has no $$. PowerShell's own parent is the batch host, so it can report the
// PID the harness needs. If PowerShell is unavailable the loop simply writes
// nothing and Dispose falls back to leaving the launcher alone.
- var recordPid = $"for /f %%p in ('powershell -NoProfile -Command \"(Get-Process -Id $PID).Parent.Id\"') do @echo %%p> \"{Path.Combine(workingDirectory, LauncherPidFileName)}\"\n";
+ var recordPid = exitImmediately
+ ? string.Empty
+ : $"for /f %%p in ('powershell -NoProfile -Command \"(Get-Process -Id $PID).Parent.Id\"') do @echo %%p> \"{Path.Combine(workingDirectory, LauncherPidFileName)}\"\n";
// Leave the working directory afterwards: a batch host holds its current directory
// open, which would defeat the cleanup delete for the launcher's whole lifetime.
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/GameProfileManagerTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/GameProfileManagerTests.cs
index f769159f2..a0803ae32 100644
--- a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/GameProfileManagerTests.cs
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/GameProfileManagerTests.cs
@@ -578,15 +578,22 @@ public async Task UpdateProfileAsync_Should_SendProfileUpdatedMessage_OnSuccessA
ProfileUpdatedMessage? receivedMessage = null;
- WeakReferenceMessenger.Default.Register(this, (_, m) => receivedMessage = m);
+ try
+ {
+ WeakReferenceMessenger.Default.Register(this, (_, m) => receivedMessage = m);
- // Act
- var result = await _profileManager.UpdateProfileAsync(profileId, request);
+ // Act
+ var result = await _profileManager.UpdateProfileAsync(profileId, request);
- // Assert
- Assert.True(result.Success);
- Assert.NotNull(receivedMessage);
- Assert.Equal("Updated Name", receivedMessage.Profile.Name);
+ // Assert
+ Assert.True(result.Success);
+ Assert.NotNull(receivedMessage);
+ Assert.Equal("Updated Name", receivedMessage.Profile.Name);
+ }
+ finally
+ {
+ WeakReferenceMessenger.Default.UnregisterAll(this);
+ }
}
private static GameInstallation CreateTestInstallation(string clientId)
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/NativeLaunchDiagnosticsTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/NativeLaunchDiagnosticsTests.cs
index 9e1cc099d..8ae7d3187 100644
--- a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/NativeLaunchDiagnosticsTests.cs
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/NativeLaunchDiagnosticsTests.cs
@@ -121,7 +121,7 @@ public async Task ChattyProcess_StillLaunchesWithoutDeadlockingAsync()
// Writes far more than a pipe buffer holds, then keeps running.
await File.WriteAllTextAsync(
binary,
- "#!/bin/sh\ni=0\nwhile [ $i -lt 2000 ]; do echo \"log line $i padding padding padding\" >&2; i=$((i+1)); done\nsleep 30\n");
+ "#!/bin/sh\ni=0\nwhile [ $i -lt 2000 ]; do echo \"log line $i padding padding padding\" >&2; i=$((i+1)); done\nsleep 2\n");
if (!OperatingSystem.IsWindows())
{
File.SetUnixFileMode(
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/ViewModels/DownloadsViewModelTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/ViewModels/DownloadsViewModelTests.cs
index c4479e34c..c6800f9c4 100644
--- a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/ViewModels/DownloadsViewModelTests.cs
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/ViewModels/DownloadsViewModelTests.cs
@@ -1,4 +1,3 @@
-using GenHub.Core.Interfaces.Common;
using GenHub.Core.Interfaces.GitHub;
using GenHub.Core.Interfaces.Notifications;
using GenHub.Features.Content.Services.ContentDiscoverers;
@@ -30,16 +29,11 @@ public async Task InitializeAsync_CompletesSuccessfullyAsync()
new Mock().Object,
new Mock>().Object);
- var mockConfigProvider = new Mock();
- mockConfigProvider.Setup(x => x.GetApplicationDataPath()).Returns(Path.GetTempPath());
- mockConfigProvider.Setup(x => x.GetWorkspacePath()).Returns(Path.Combine(Path.GetTempPath(), "GenHubWorkspaces"));
-
var vm = new DownloadsViewModel(
mockServiceProvider.Object,
mockLogger.Object,
mockNotificationService.Object,
- discoverer,
- mockConfigProvider.Object);
+ discoverer);
// Act
await vm.InitializeAsync();
@@ -47,4 +41,4 @@ public async Task InitializeAsync_CompletesSuccessfullyAsync()
// Assert
Assert.NotNull(vm);
}
-}
\ No newline at end of file
+}
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/ViewModels/GameProfileSettingsViewModelTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/ViewModels/GameProfileSettingsViewModelTests.cs
index 4ad893ac4..cb6143239 100644
--- a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/ViewModels/GameProfileSettingsViewModelTests.cs
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/ViewModels/GameProfileSettingsViewModelTests.cs
@@ -159,7 +159,7 @@ public async Task InitializeForProfileAsync_WithoutProfileManager_SetsLoadingErr
///
/// Verifies that receiving a updates enabled content without duplication.
///
- /// A task representing the asynchronous test.
+ /// A task representing the asynchronous unit test.
[Fact]
public async Task ReceiveManifestReplacedMessage_UpdatesEnabledContent_WithoutDuplicationAsync()
{
@@ -181,35 +181,39 @@ public async Task ReceiveManifestReplacedMessage_UpdatesEnabledContent_WithoutDu
InstallationType = GenHub.Core.Models.Enums.GameInstallationType.Steam,
};
- var newManifest = new ContentManifest
+ var manifest = new ContentManifest
{
Id = GenHub.Core.Models.Manifest.ManifestId.Create(newId),
Name = "My Mod v2",
- ContentType = GenHub.Core.Models.Enums.ContentType.Mod,
Version = "2.0",
+ ContentType = GenHub.Core.Models.Enums.ContentType.Mod,
+ TargetGame = GenHub.Core.Models.Enums.GameType.Generals,
};
mockManifestPool
- .Setup(x => x.GetManifestAsync(It.Is(id => id.Value == newId), It.IsAny()))
- .ReturnsAsync(OperationResult.CreateSuccess(newManifest));
+ .Setup(m => m.GetManifestAsync(It.Is(id => id.Value == newId), It.IsAny()))
+ .ReturnsAsync(GenHub.Core.Models.Results.OperationResult.CreateSuccess(manifest));
mockContentLoader
- .Setup(x => x.CreateManifestDisplayItem(
- It.Is(m => m.Id.Value == newId),
- It.IsAny(),
- It.IsAny(),
- It.IsAny()))
- .Returns(new CoreContentDisplayItem
+ .Setup(c => c.CreateManifestDisplayItem(manifest, It.IsAny(), It.IsAny(), It.IsAny()))
+ .Returns(new GenHub.Core.Models.Content.ContentDisplayItem
{
Id = newId,
ManifestId = newId,
DisplayName = "My Mod v2",
- Version = "2.0",
+ IsEnabled = true,
ContentType = GenHub.Core.Models.Enums.ContentType.Mod,
GameType = GenHub.Core.Models.Enums.GameType.Generals,
InstallationType = GenHub.Core.Models.Enums.GameInstallationType.Steam,
});
+ mockContentLoader
+ .Setup(c => c.LoadAvailableContentAsync(
+ It.IsAny(),
+ It.IsAny>(),
+ It.IsAny>()))
+ .ReturnsAsync(new System.Collections.ObjectModel.ObservableCollection());
+
var logger = NullLogger.Instance;
var vm = new GameProfileSettingsViewModel(
null, // gameProfileManager
@@ -229,8 +233,7 @@ public async Task ReceiveManifestReplacedMessage_UpdatesEnabledContent_WithoutDu
// Directly populate the EnabledContent collection to simulate state
vm.EnabledContent.Add(oldItem);
- // Act - call handler directly to avoid Dispatcher issues in test
- // WeakReferenceMessenger.Default.Send(new ManifestReplacedMessage(oldId, newId));
+ // Act
await vm.HandleManifestReplacementAsync(oldId, newId);
// Assert
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/ViewModels/MainViewModelTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/ViewModels/MainViewModelTests.cs
index c476236bf..167dbfe2d 100644
--- a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/ViewModels/MainViewModelTests.cs
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/ViewModels/MainViewModelTests.cs
@@ -271,8 +271,7 @@ private static DownloadsViewModel CreateDownloadsViewModel(IConfigurationProvide
mockServiceProvider.Object,
mockLogger.Object,
mockNotificationService.Object,
- realGitHubDiscoverer,
- configProvider);
+ realGitHubDiscoverer);
}
private static GameProfileLauncherViewModel CreateGameProfileLauncherViewModel()
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Tools/ModBuilder/Converters/ModBuilderConverterTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Tools/ModBuilder/Converters/ModBuilderConverterTests.cs
new file mode 100644
index 000000000..265e11354
--- /dev/null
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Tools/ModBuilder/Converters/ModBuilderConverterTests.cs
@@ -0,0 +1,79 @@
+//
+// Copyright (c) Enowx Labs. All rights reserved.
+//
+
+namespace GenHub.Tests.Core.Features.Tools.ModBuilder.Converters;
+
+using System;
+using System.Globalization;
+using Avalonia;
+using Avalonia.Media;
+using GenHub.Infrastructure.Converters;
+using Xunit;
+
+///
+/// Unit tests for ModBuilder XAML value converters.
+///
+public class ModBuilderConverterTests
+{
+ [Fact]
+ public void ActiveBorderConverter_WhenActive_ReturnsCyanBrush()
+ {
+ var converter = new ActiveBorderConverter();
+
+ var result = converter.Convert(true, typeof(IBrush), null, CultureInfo.InvariantCulture);
+
+ Assert.NotNull(result);
+ var brush = Assert.IsAssignableFrom(result);
+ Assert.Equal(Color.Parse("#00D9FF"), brush.Color);
+ }
+
+ [Fact]
+ public void ActiveBorderConverter_WhenInactive_ReturnsDefaultBrush()
+ {
+ var converter = new ActiveBorderConverter();
+
+ var result = converter.Convert(false, typeof(IBrush), null, CultureInfo.InvariantCulture);
+
+ Assert.NotNull(result);
+ var brush = Assert.IsAssignableFrom(result);
+ Assert.Equal(Color.Parse("#20FFFFFF"), brush.Color);
+ }
+
+ [Fact]
+ public void ActiveBorderConverter_ConvertBack_ThrowsNotSupportedException()
+ {
+ var converter = new ActiveBorderConverter();
+
+ Assert.Throws(() =>
+ converter.ConvertBack(null, typeof(bool), null, CultureInfo.InvariantCulture));
+ }
+
+ [Theory]
+ [InlineData(0, 0)]
+ [InlineData(1, 16)]
+ [InlineData(2, 32)]
+ [InlineData(3, 48)]
+ public void IndentConverter_GivenIndentLevel_ReturnsExpectedLeftMargin(int level, double expectedLeft)
+ {
+ var converter = new IndentConverter();
+
+ var result = converter.Convert(level, typeof(Thickness), null, CultureInfo.InvariantCulture);
+
+ Assert.NotNull(result);
+ var thickness = Assert.IsType(result);
+ Assert.Equal(expectedLeft, thickness.Left);
+ Assert.Equal(0, thickness.Top);
+ Assert.Equal(0, thickness.Right);
+ Assert.Equal(0, thickness.Bottom);
+ }
+
+ [Fact]
+ public void IndentConverter_ConvertBack_ThrowsNotSupportedException()
+ {
+ var converter = new IndentConverter();
+
+ Assert.Throws(() =>
+ converter.ConvertBack(null, typeof(int), null, CultureInfo.InvariantCulture));
+ }
+}
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Tools/ModBuilder/Services/ArchiveServiceTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Tools/ModBuilder/Services/ArchiveServiceTests.cs
new file mode 100644
index 000000000..c095b7453
--- /dev/null
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Tools/ModBuilder/Services/ArchiveServiceTests.cs
@@ -0,0 +1,294 @@
+using System;
+using System.IO;
+using System.IO.Compression;
+using System.Threading;
+using System.Threading.Tasks;
+using FluentAssertions;
+using GenHub.Features.Tools.ModBuilder.Services;
+using Microsoft.Extensions.Logging;
+using Moq;
+
+namespace GenHub.Tests.Core.Features.Tools.ModBuilder.Services;
+
+///
+/// Unit tests for .
+///
+public sealed class ArchiveServiceTests : IDisposable
+{
+ private readonly Mock> _mockLogger;
+ private readonly ArchiveService _service;
+ private readonly string _tempDirectory;
+
+ public ArchiveServiceTests()
+ {
+ _mockLogger = new Mock>();
+ _service = new ArchiveService(_mockLogger.Object);
+ _tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(_tempDirectory);
+ }
+
+ public void Dispose()
+ {
+ if (Directory.Exists(_tempDirectory))
+ {
+ Directory.Delete(_tempDirectory, recursive: true);
+ }
+ }
+
+ [Fact]
+ public void Constructor_WithValidDependencies_DoesNotThrow()
+ {
+ // Act
+ var service = new ArchiveService(_mockLogger.Object);
+
+ // Assert
+ service.Should().NotBeNull();
+ }
+
+ [Fact]
+ public async Task CreateZipArchiveAsync_WithValidDirectory_CreatesZip()
+ {
+ // Arrange
+ var sourceDir = Path.Combine(_tempDirectory, "source");
+ Directory.CreateDirectory(sourceDir);
+ await File.WriteAllTextAsync(Path.Combine(sourceDir, "file1.txt"), "content1");
+ await File.WriteAllTextAsync(Path.Combine(sourceDir, "file2.txt"), "content2");
+
+ var targetZip = Path.Combine(_tempDirectory, "output.zip");
+
+ // Act
+ var result = await _service.CreateZipArchiveAsync(sourceDir, targetZip);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Success.Should().BeTrue();
+ File.Exists(targetZip).Should().BeTrue();
+ }
+
+ [Fact]
+ public async Task CreateZipArchiveAsync_WithNonExistentDirectory_ReturnsFailure()
+ {
+ // Arrange
+ var sourceDir = Path.Combine(_tempDirectory, "nonexistent");
+ var targetZip = Path.Combine(_tempDirectory, "output.zip");
+
+ // Act
+ var result = await _service.CreateZipArchiveAsync(sourceDir, targetZip);
+
+ // Assert
+ result.Success.Should().BeFalse();
+ result.Errors.Should().Contain(e => e.Contains("not found"));
+ }
+
+ [Fact]
+ public async Task CreateZipArchiveAsync_WithCompressionLevel_UsesSpecifiedLevel()
+ {
+ // Arrange
+ var sourceDir = Path.Combine(_tempDirectory, "source");
+ Directory.CreateDirectory(sourceDir);
+ await File.WriteAllTextAsync(Path.Combine(sourceDir, "file.txt"), "content");
+
+ var targetZip = Path.Combine(_tempDirectory, "output.zip");
+
+ // Act
+ var result = await _service.CreateZipArchiveAsync(sourceDir, targetZip, CompressionLevel.Fastest);
+
+ // Assert
+ result.Success.Should().BeTrue();
+ File.Exists(targetZip).Should().BeTrue();
+ }
+
+ [Fact]
+ public async Task CreateZipArchiveAsync_WithProgress_ReportsProgress()
+ {
+ // Arrange
+ var sourceDir = Path.Combine(_tempDirectory, "source");
+ Directory.CreateDirectory(sourceDir);
+ await File.WriteAllTextAsync(Path.Combine(sourceDir, "file.txt"), "content");
+
+ var targetZip = Path.Combine(_tempDirectory, "output.zip");
+ var progressMock = new Mock>();
+
+ // Act
+ var result = await _service.CreateZipArchiveAsync(sourceDir, targetZip, progress: progressMock.Object);
+
+ // Assert
+ result.Success.Should().BeTrue();
+ progressMock.Verify(p => p.Report(It.IsAny()), Times.AtLeastOnce());
+ }
+
+ [Fact]
+ public async Task CreateZipArchiveAsync_WithExistingFile_OverwritesFile()
+ {
+ // Arrange
+ var sourceDir = Path.Combine(_tempDirectory, "source");
+ Directory.CreateDirectory(sourceDir);
+ await File.WriteAllTextAsync(Path.Combine(sourceDir, "file.txt"), "content");
+
+ var targetZip = Path.Combine(_tempDirectory, "output.zip");
+ await File.WriteAllTextAsync(targetZip, "old content");
+
+ // Act
+ var result = await _service.CreateZipArchiveAsync(sourceDir, targetZip);
+
+ // Assert
+ result.Success.Should().BeTrue();
+ File.Exists(targetZip).Should().BeTrue();
+ }
+
+ [Fact]
+ public async Task CreateZipArchiveAsync_WithNestedDirectories_IncludesAllFiles()
+ {
+ // Arrange
+ var sourceDir = Path.Combine(_tempDirectory, "source");
+ var subDir = Path.Combine(sourceDir, "subdir");
+ Directory.CreateDirectory(subDir);
+ await File.WriteAllTextAsync(Path.Combine(sourceDir, "file1.txt"), "content1");
+ await File.WriteAllTextAsync(Path.Combine(subDir, "file2.txt"), "content2");
+
+ var targetZip = Path.Combine(_tempDirectory, "output.zip");
+
+ // Act
+ var result = await _service.CreateZipArchiveAsync(sourceDir, targetZip);
+
+ // Assert
+ result.Success.Should().BeTrue();
+ using var archive = ZipFile.OpenRead(targetZip);
+ archive.Entries.Should().HaveCountGreaterOrEqualTo(2);
+ }
+
+ [Fact]
+ public async Task CreateTarArchiveAsync_WithValidDirectory_CreatesTar()
+ {
+ // Arrange
+ var sourceDir = Path.Combine(_tempDirectory, "source");
+ Directory.CreateDirectory(sourceDir);
+ await File.WriteAllTextAsync(Path.Combine(sourceDir, "file.txt"), "content");
+
+ var targetTar = Path.Combine(_tempDirectory, "output.tar");
+
+ // Act
+ var result = await _service.CreateTarArchiveAsync(sourceDir, targetTar);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Success.Should().BeTrue();
+ File.Exists(targetTar).Should().BeTrue();
+ }
+
+ [Fact]
+ public async Task CreateTarArchiveAsync_WithNonExistentDirectory_ReturnsFailure()
+ {
+ // Arrange
+ var sourceDir = Path.Combine(_tempDirectory, "nonexistent");
+ var targetTar = Path.Combine(_tempDirectory, "output.tar");
+
+ // Act
+ var result = await _service.CreateTarArchiveAsync(sourceDir, targetTar);
+
+ // Assert
+ result.Success.Should().BeFalse();
+ result.Errors.Should().Contain(e => e.Contains("not found"));
+ }
+
+ [Fact]
+ public async Task CreateTarGzArchiveAsync_WithValidDirectory_CreatesTarGz()
+ {
+ // Arrange
+ var sourceDir = Path.Combine(_tempDirectory, "source");
+ Directory.CreateDirectory(sourceDir);
+ await File.WriteAllTextAsync(Path.Combine(sourceDir, "file.txt"), "content");
+
+ var targetTarGz = Path.Combine(_tempDirectory, "output.tar.gz");
+
+ // Act
+ var result = await _service.CreateTarGzArchiveAsync(sourceDir, targetTarGz);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Success.Should().BeTrue();
+ File.Exists(targetTarGz).Should().BeTrue();
+ }
+
+ [Fact]
+ public async Task CreateTarGzArchiveAsync_WithNonExistentDirectory_ReturnsFailure()
+ {
+ // Arrange
+ var sourceDir = Path.Combine(_tempDirectory, "nonexistent");
+ var targetTarGz = Path.Combine(_tempDirectory, "output.tar.gz");
+
+ // Act
+ var result = await _service.CreateTarGzArchiveAsync(sourceDir, targetTarGz);
+
+ // Assert
+ result.Success.Should().BeFalse();
+ result.Errors.Should().Contain(e => e.Contains("not found"));
+ }
+
+ [Fact]
+ public async Task CreateBigArchiveAsync_WithValidDirectory_CreatesBig()
+ {
+ // Arrange
+ var sourceDir = Path.Combine(_tempDirectory, "source");
+ Directory.CreateDirectory(sourceDir);
+ await File.WriteAllTextAsync(Path.Combine(sourceDir, "file.txt"), "content");
+
+ var targetBig = Path.Combine(_tempDirectory, "output.big");
+
+ // Act
+ var result = await _service.CreateBigArchiveAsync(sourceDir, targetBig);
+
+ // Assert
+ result.Should().NotBeNull();
+ // BIG archive creation may require specific tools, so we just check the result structure
+ }
+
+ [Fact]
+ public async Task CreateBigArchiveAsync_WithNonExistentDirectory_ReturnsFailure()
+ {
+ // Arrange
+ var sourceDir = Path.Combine(_tempDirectory, "nonexistent");
+ var targetBig = Path.Combine(_tempDirectory, "output.big");
+
+ // Act
+ var result = await _service.CreateBigArchiveAsync(sourceDir, targetBig);
+
+ // Assert
+ result.Success.Should().BeFalse();
+ result.Errors.Should().Contain(e => e.Contains("not found"));
+ }
+
+ [Fact]
+ public async Task CreateZipArchiveAsync_WithCancellation_ThrowsOperationCanceledException()
+ {
+ // Arrange
+ var sourceDir = Path.Combine(_tempDirectory, "source");
+ Directory.CreateDirectory(sourceDir);
+ await File.WriteAllTextAsync(Path.Combine(sourceDir, "file.txt"), "content");
+
+ var targetZip = Path.Combine(_tempDirectory, "output.zip");
+ var cts = new CancellationTokenSource();
+ cts.Cancel();
+
+ // Act & Assert
+ await Assert.ThrowsAnyAsync(
+ async () => await _service.CreateZipArchiveAsync(sourceDir, targetZip, cancellationToken: cts.Token));
+ }
+
+ [Fact]
+ public async Task CreateZipArchiveAsync_WithEmptyDirectory_CreatesEmptyZip()
+ {
+ // Arrange
+ var sourceDir = Path.Combine(_tempDirectory, "empty");
+ Directory.CreateDirectory(sourceDir);
+
+ var targetZip = Path.Combine(_tempDirectory, "output.zip");
+
+ // Act
+ var result = await _service.CreateZipArchiveAsync(sourceDir, targetZip);
+
+ // Assert
+ result.Success.Should().BeTrue();
+ File.Exists(targetZip).Should().BeTrue();
+ }
+}
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Tools/ModBuilder/Services/BuildCacheServiceTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Tools/ModBuilder/Services/BuildCacheServiceTests.cs
new file mode 100644
index 000000000..4f0c7698f
--- /dev/null
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Tools/ModBuilder/Services/BuildCacheServiceTests.cs
@@ -0,0 +1,324 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using FluentAssertions;
+using GenHub.Core.Interfaces.Tools.ModBuilder;
+using GenHub.Core.Models.Tools.ModBuilder;
+using GenHub.Features.Tools.ModBuilder.Services;
+using Microsoft.Extensions.Logging;
+using Moq;
+
+namespace GenHub.Tests.Core.Features.Tools.ModBuilder.Services;
+
+///
+/// Unit tests for .
+///
+public sealed class BuildCacheServiceTests : IDisposable
+{
+ private readonly Mock _mockMd5Provider;
+ private readonly Mock _mockRegistryService;
+ private readonly Mock> _mockLogger;
+ private readonly string _tempDirectory;
+ private readonly BuildCacheService _service;
+
+ public BuildCacheServiceTests()
+ {
+ _mockMd5Provider = new Mock();
+ _mockRegistryService = new Mock();
+ _mockLogger = new Mock>();
+ _tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(_tempDirectory);
+
+ _service = new BuildCacheService(
+ _mockMd5Provider.Object,
+ _mockLogger.Object,
+ _mockRegistryService.Object);
+ }
+
+ public void Dispose()
+ {
+ if (Directory.Exists(_tempDirectory))
+ {
+ Directory.Delete(_tempDirectory, recursive: true);
+ }
+ }
+
+ [Fact]
+ public void Constructor_WithValidDependencies_DoesNotThrow()
+ {
+ // Act
+ var service = new BuildCacheService(_mockMd5Provider.Object, _mockLogger.Object);
+
+ // Assert
+ service.Should().NotBeNull();
+ }
+
+ [Fact]
+ public async Task LoadCacheAsync_WhenFileDoesNotExist_ReturnsFalse()
+ {
+ // Arrange
+ var nonExistentPath = Path.Combine(_tempDirectory, "nonexistent.json");
+
+ // Act
+ var result = await _service.LoadCacheAsync(nonExistentPath);
+
+ // Assert
+ result.Should().BeFalse();
+ }
+
+ [Fact]
+ public async Task SaveCacheAsync_CreatesDirectoryIfNotExists()
+ {
+ // Arrange
+ var cachePath = Path.Combine(_tempDirectory, "subdir", "cache.json");
+ _service.AddFile("test.txt", 123.45, "abc123");
+
+ // Act
+ var result = await _service.SaveCacheAsync(cachePath);
+
+ // Assert
+ result.Should().BeTrue();
+ Directory.Exists(Path.GetDirectoryName(cachePath)).Should().BeTrue();
+ }
+
+ [Fact]
+ public async Task SaveAndLoadCache_MessagePackFormat_PreservesData()
+ {
+ // Arrange
+ var cachePath = Path.Combine(_tempDirectory, "cache.json");
+ _service.AddFile("file1.txt", 100.0, "hash1");
+ _service.AddFile("file2.txt", 200.0, "hash2", new Dictionary { ["key"] = "value" });
+
+ // Act - Save
+ var saveResult = await _service.SaveCacheAsync(cachePath);
+
+ // Create new service to load
+ var loadService = new BuildCacheService(_mockMd5Provider.Object, _mockLogger.Object);
+ var loadResult = await loadService.LoadCacheAsync(cachePath);
+
+ // Assert
+ saveResult.Should().BeTrue();
+ loadResult.Should().BeTrue();
+
+ var file1 = loadService.FindOldFile("file1.txt");
+ file1.Should().NotBeNull();
+ file1!.Md5.Should().Be("hash1");
+ file1.ModifiedTime.Should().Be(100.0);
+
+ var file2 = loadService.FindOldFile("file2.txt");
+ file2.Should().NotBeNull();
+ file2!.Params.Should().ContainKey("key");
+ }
+
+ [Fact]
+ public void AddFile_StoresFileInCache()
+ {
+ // Act
+ _service.AddFile("test.txt", 123.45, "abc123");
+
+ // Assert - Verify by checking if we can find it after save/load cycle
+ _service.FindOldFile("test.txt").Should().BeNull(); // Not in old cache yet
+ }
+
+ [Fact]
+ public async Task FindOldFile_WhenFileExists_ReturnsInfo()
+ {
+ // Arrange
+ var cachePath = Path.Combine(_tempDirectory, "cache.json");
+ _service.AddFile("test.txt", 123.45, "abc123");
+ await _service.SaveCacheAsync(cachePath);
+
+ var newService = new BuildCacheService(_mockMd5Provider.Object, _mockLogger.Object);
+ await newService.LoadCacheAsync(cachePath);
+
+ // Act
+ var result = newService.FindOldFile("test.txt");
+
+ // Assert
+ result.Should().NotBeNull();
+ result!.Path.Should().Be("test.txt");
+ result.Md5.Should().Be("abc123");
+ result.ModifiedTime.Should().Be(123.45);
+ }
+
+ [Fact]
+ public void FindOldFile_WhenFileDoesNotExist_ReturnsNull()
+ {
+ // Act
+ var result = _service.FindOldFile("nonexistent.txt");
+
+ // Assert
+ result.Should().BeNull();
+ }
+
+ [Fact]
+ public async Task FindOldFile_IsCaseInsensitive()
+ {
+ // Arrange
+ var cachePath = Path.Combine(_tempDirectory, "cache.json");
+ _service.AddFile("Test.TXT", 123.45, "abc123");
+ await _service.SaveCacheAsync(cachePath);
+
+ var newService = new BuildCacheService(_mockMd5Provider.Object, _mockLogger.Object);
+ await newService.LoadCacheAsync(cachePath);
+
+ // Act
+ var result = newService.FindOldFile("test.txt");
+
+ // Assert
+ result.Should().NotBeNull();
+ }
+
+ [Fact]
+ public async Task ComputeOrReuseMd5Async_WhenFileNotInCache_ComputesNewHash()
+ {
+ // Arrange
+ var testFile = Path.Combine(_tempDirectory, "test.txt");
+ await File.WriteAllTextAsync(testFile, "content");
+ _mockMd5Provider.Setup(x => x.ComputeFileHashAsync(testFile, It.IsAny()))
+ .ReturnsAsync("newhash");
+
+ // Act
+ var result = await _service.ComputeOrReuseMd5Async(testFile);
+
+ // Assert
+ result.Should().Be("newhash");
+ _mockMd5Provider.Verify(x => x.ComputeFileHashAsync(testFile, It.IsAny()), Times.Once);
+ }
+
+ [Fact]
+ public void DetermineFileStatus_WhenFileInRegistry_ReturnsIrrelevant()
+ {
+ // Arrange
+ _mockRegistryService.Setup(x => x.IsFileIrrelevant("test.txt", "hash123"))
+ .Returns(true);
+
+ // Act
+ var result = _service.DetermineFileStatus("test.txt", "hash123");
+
+ // Assert
+ result.Should().Be(BuildFileStatus.Irrelevant);
+ }
+
+ [Fact]
+ public void DetermineFileStatus_WhenFileNotInCache_ReturnsAdded()
+ {
+ // Arrange
+ _mockRegistryService.Setup(x => x.IsFileIrrelevant(It.IsAny(), It.IsAny()))
+ .Returns(false);
+
+ // Act
+ var result = _service.DetermineFileStatus("newfile.txt", "hash123");
+
+ // Assert
+ result.Should().Be(BuildFileStatus.Added);
+ }
+
+ [Fact]
+ public async Task DetermineFileStatus_WhenHashMatches_ReturnsUnchanged()
+ {
+ // Arrange
+ var cachePath = Path.Combine(_tempDirectory, "cache.json");
+ _service.AddFile("test.txt", 123.45, "hash123");
+ await _service.SaveCacheAsync(cachePath);
+
+ var newService = new BuildCacheService(_mockMd5Provider.Object, _mockLogger.Object, _mockRegistryService.Object);
+ await newService.LoadCacheAsync(cachePath);
+
+ _mockRegistryService.Setup(x => x.IsFileIrrelevant(It.IsAny(), It.IsAny()))
+ .Returns(false);
+
+ // Act
+ var result = newService.DetermineFileStatus("test.txt", "hash123");
+
+ // Assert
+ result.Should().Be(BuildFileStatus.Unchanged);
+ }
+
+ [Fact]
+ public async Task DetermineFileStatus_WhenHashDiffers_ReturnsChanged()
+ {
+ // Arrange
+ var cachePath = Path.Combine(_tempDirectory, "cache.json");
+ _service.AddFile("test.txt", 123.45, "oldhash");
+ await _service.SaveCacheAsync(cachePath);
+
+ var newService = new BuildCacheService(_mockMd5Provider.Object, _mockLogger.Object, _mockRegistryService.Object);
+ await newService.LoadCacheAsync(cachePath);
+
+ _mockRegistryService.Setup(x => x.IsFileIrrelevant(It.IsAny(), It.IsAny()))
+ .Returns(false);
+
+ // Act
+ var result = newService.DetermineFileStatus("test.txt", "newhash");
+
+ // Assert
+ result.Should().Be(BuildFileStatus.Changed);
+ }
+
+ [Fact]
+ public async Task DetermineFileStatus_WhenParamsDiffer_ReturnsChanged()
+ {
+ // Arrange
+ var cachePath = Path.Combine(_tempDirectory, "cache.json");
+ _service.AddFile("test.txt", 123.45, "hash123", new Dictionary { ["key"] = "oldvalue" });
+ await _service.SaveCacheAsync(cachePath);
+
+ var newService = new BuildCacheService(_mockMd5Provider.Object, _mockLogger.Object, _mockRegistryService.Object);
+ await newService.LoadCacheAsync(cachePath);
+
+ _mockRegistryService.Setup(x => x.IsFileIrrelevant(It.IsAny(), It.IsAny()))
+ .Returns(false);
+
+ // Act
+ var result = newService.DetermineFileStatus("test.txt", "hash123", new Dictionary { ["key"] = "newvalue" });
+
+ // Assert
+ result.Should().Be(BuildFileStatus.Changed);
+ }
+
+ [Fact]
+ public void Clear_RemovesAllCacheEntries()
+ {
+ // Arrange
+ _service.AddFile("file1.txt", 100.0, "hash1");
+ _service.AddFile("file2.txt", 200.0, "hash2");
+
+ // Act
+ _service.Clear();
+
+ // Assert
+ _service.FindOldFile("file1.txt").Should().BeNull();
+ _service.FindOldFile("file2.txt").Should().BeNull();
+ }
+
+ [Fact]
+ public async Task LoadCacheAsync_WithInvalidJson_ReturnsFalse()
+ {
+ // Arrange
+ var cachePath = Path.Combine(_tempDirectory, "invalid.json");
+ await File.WriteAllTextAsync(cachePath, "{ invalid json }");
+
+ // Act
+ var result = await _service.LoadCacheAsync(cachePath);
+
+ // Assert
+ result.Should().BeFalse();
+ }
+
+ [Fact]
+ public async Task SaveCacheAsync_WithCancellation_ThrowsOperationCanceledException()
+ {
+ // Arrange
+ var cachePath = Path.Combine(_tempDirectory, "cache.json");
+ _service.AddFile("test.txt", 123.45, "abc123");
+ var cts = new CancellationTokenSource();
+ cts.Cancel();
+
+ // Act & Assert
+ await Assert.ThrowsAnyAsync(
+ async () => await _service.SaveCacheAsync(cachePath, cts.Token));
+ }
+}
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Tools/ModBuilder/Services/BuildEngineServiceTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Tools/ModBuilder/Services/BuildEngineServiceTests.cs
new file mode 100644
index 000000000..742025c04
--- /dev/null
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Tools/ModBuilder/Services/BuildEngineServiceTests.cs
@@ -0,0 +1,631 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using FluentAssertions;
+using GenHub.Core.Interfaces.Tools.ModBuilder;
+using GenHub.Core.Models.Tools.ModBuilder;
+using GenHub.Features.Tools.ModBuilder.Services;
+using Microsoft.Extensions.Logging;
+using Moq;
+
+namespace GenHub.Tests.Core.Features.Tools.ModBuilder.Services;
+
+///
+/// Unit tests for .
+///
+public sealed class BuildEngineServiceTests : IDisposable
+{
+ private readonly Mock _mockCacheService;
+ private readonly Mock _mockFileConversionService;
+ private readonly Mock _mockHashProvider;
+ private readonly Mock _mockConfigurationLoaderService;
+ private readonly Mock _mockArchiveService;
+ private readonly Mock> _mockLogger;
+ private readonly BuildEngineService _service;
+ private readonly string _tempDirectory;
+
+ public BuildEngineServiceTests()
+ {
+ _mockCacheService = new Mock();
+ _mockFileConversionService = new Mock();
+ _mockHashProvider = new Mock();
+ _mockConfigurationLoaderService = new Mock();
+ _mockArchiveService = new Mock();
+ _mockLogger = new Mock>();
+ _tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(_tempDirectory);
+
+ _mockConfigurationLoaderService.Setup(x => x.ResolveWildcardsAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync((BuildConfiguration config, CancellationToken ct) => config);
+
+ _mockArchiveService.Setup(x => x.CreateBigArchiveAsync(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny()))
+ .ReturnsAsync(GenHub.Core.Models.Results.OperationResult.CreateSuccess(true));
+
+ _mockArchiveService.Setup(x => x.CreateZipArchiveAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny()))
+ .ReturnsAsync(GenHub.Core.Models.Results.OperationResult.CreateSuccess(true));
+
+ _service = new BuildEngineService(
+ _mockCacheService.Object,
+ _mockFileConversionService.Object,
+ _mockHashProvider.Object,
+ _mockConfigurationLoaderService.Object,
+ _mockArchiveService.Object,
+ _mockLogger.Object);
+ }
+
+ public void Dispose()
+ {
+ if (Directory.Exists(_tempDirectory))
+ {
+ Directory.Delete(_tempDirectory, recursive: true);
+ }
+ }
+
+ [Fact]
+ public void Constructor_WithValidDependencies_DoesNotThrow()
+ {
+ // Act
+ var service = new BuildEngineService(
+ _mockCacheService.Object,
+ _mockFileConversionService.Object,
+ _mockHashProvider.Object,
+ _mockConfigurationLoaderService.Object,
+ _mockArchiveService.Object,
+ _mockLogger.Object);
+
+ // Assert
+ service.Should().NotBeNull();
+ }
+
+ [Fact]
+ public async Task ExecuteBuildAsync_WithValidProject_ReturnsSuccess()
+ {
+ // Arrange
+ var project = new ModBuilderProject
+ {
+ Name = "TestProject",
+ Directories = new ProjectDirectories
+ {
+ GameFilesEdited = _tempDirectory,
+ Build = Path.Combine(_tempDirectory, "output")
+ },
+ BundleConfigs = new List()
+ };
+
+ var configuration = new BuildConfiguration
+ {
+ Items = new List(),
+ Packs = new List()
+ };
+
+ var selectedPacks = new List();
+
+ // Act
+ var result = await _service.ExecuteBuildAsync(project, configuration, selectedPacks, BuildStep.Build);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Success.Should().BeTrue();
+ }
+
+ [Fact]
+ public async Task ExecuteBuildAsync_WithNullProject_ThrowsException()
+ {
+ // Arrange
+ ModBuilderProject? project = null;
+ var configuration = new BuildConfiguration();
+ var selectedPacks = new List();
+
+ // Act & Assert
+ await Assert.ThrowsAsync(
+ async () => await _service.ExecuteBuildAsync(project!, configuration, selectedPacks, BuildStep.Build));
+ }
+
+ [Fact]
+ public async Task ExecuteBuildAsync_WithProgress_ReportsProgress()
+ {
+ // Arrange
+ var project = new ModBuilderProject
+ {
+ Name = "TestProject",
+ Directories = new ProjectDirectories
+ {
+ GameFilesEdited = _tempDirectory,
+ Build = Path.Combine(_tempDirectory, "output")
+ },
+ BundleConfigs = new List()
+ };
+
+ var configuration = new BuildConfiguration
+ {
+ Items = new List(),
+ Packs = new List()
+ };
+
+ var selectedPacks = new List