diff --git a/GenHub/CsvGenerationUtility/CsvGenerationUtility.csproj b/GenHub/CsvGenerationUtility/CsvGenerationUtility.csproj
new file mode 100644
index 00000000..95a1accd
--- /dev/null
+++ b/GenHub/CsvGenerationUtility/CsvGenerationUtility.csproj
@@ -0,0 +1,30 @@
+
+
+
+ Exe
+ net8.0-windows
+ enable
+ enable
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
diff --git a/GenHub/CsvGenerationUtility/Program.cs b/GenHub/CsvGenerationUtility/Program.cs
new file mode 100644
index 00000000..890cf233
--- /dev/null
+++ b/GenHub/CsvGenerationUtility/Program.cs
@@ -0,0 +1,521 @@
+
+
+using System.Security.Cryptography;
+using CsvHelper;
+using CsvHelper.Configuration;
+using GenHub.Core.Models.Content;
+using GenHub.Core.Models.Enums;
+using GenHub.Core.Models.GameInstallations;
+using GenHub.Core.Models.Results;
+using GenHub.Windows.GameInstallations;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Console;
+using Newtonsoft.Json;
+
+namespace CsvGenerationUtility;
+// StyleCop suppressions for utility classes in single file
+//
+#pragma warning disable SA1402 // File may only contain a single type
+#pragma warning disable SA1516 // Elements should be separated by blank line
+
+///
+/// CSV Generation Utility for creating authoritative CSV files from game installations.
+///
+internal static class Program
+{
+ ///
+ /// Main entry point for the CSV Generation Utility.
+ ///
+ /// Command line arguments.
+ /// A task representing the asynchronous operation.
+ private static async Task Main(string[] args)
+ {
+ var configuration = BuildConfiguration();
+ var logger = CreateLogger();
+
+ try
+ {
+ logger.LogInformation("Starting CSV Generation Utility");
+
+ var generator = new CsvGenerator(configuration, logger);
+ await generator.GenerateAllCsvFilesAsync();
+
+ logger.LogInformation("CSV Generation Utility completed successfully");
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "CSV Generation Utility failed");
+ Environment.Exit(1);
+ }
+ }
+
+ private static IConfiguration BuildConfiguration()
+ {
+ return new ConfigurationBuilder()
+ .SetBasePath(Directory.GetCurrentDirectory())
+ .AddJsonFile("appsettings.json", optional: true)
+ .Build();
+ }
+
+ private static ILogger CreateLogger()
+ {
+ using var loggerFactory = LoggerFactory.Create(builder =>
+ {
+ builder.AddConsole();
+ builder.SetMinimumLevel(LogLevel.Information);
+ });
+
+ return loggerFactory.CreateLogger("CsvGenerator");
+ }
+}
+
+///
+/// Generates CSV files from game installations.
+///
+internal class CsvGenerator
+{
+ private readonly IConfiguration configuration;
+ private readonly ILogger logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The configuration.
+ /// The logger.
+ public CsvGenerator(IConfiguration configuration, ILogger logger)
+ {
+ this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
+ this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ ///
+ /// Generates CSV files for all supported games.
+ ///
+ /// A task representing the asynchronous operation.
+ public async Task GenerateAllCsvFilesAsync()
+ {
+ var games = new[]
+ {
+ (GameType.Generals, "1.08", "Generals-1.08.csv"),
+ (GameType.ZeroHour, "1.04", "ZeroHour-1.04.csv"),
+ };
+
+ foreach (var (gameType, version, csvFileName) in games)
+ {
+ await this.GenerateCsvForGameAsync(gameType, version, csvFileName);
+ }
+
+ await this.UpdateIndexJsonAsync();
+ }
+
+ private async Task GenerateCsvForGameAsync(GameType gameType, string version, string csvFileName)
+ {
+ this.logger.LogInformation("Generating CSV for {GameType} {Version}", gameType, version);
+
+ var installationPath = this.GetInstallationPath(gameType);
+ if (string.IsNullOrEmpty(installationPath) || !Directory.Exists(installationPath))
+ {
+ this.logger.LogWarning("Installation path not found for {GameType}: {Path}", gameType, installationPath);
+ return;
+ }
+
+ var csvEntries = await this.ScanInstallationAsync(installationPath, gameType);
+ var csvPath = Path.Combine(this.GetRegistryDirectory(), csvFileName);
+
+ await this.WriteCsvFileAsync(csvEntries, csvPath);
+ this.logger.LogInformation("Generated CSV file: {Path} with {Count} entries", csvPath, csvEntries.Count);
+ }
+
+ private string? GetInstallationPath(GameType gameType)
+ {
+ this.logger.LogInformation("Detecting installation path for {GameType}", gameType);
+
+ try
+ {
+ // Use the Windows installation detector to find actual installations
+ var detectorLogger = LoggerFactory.Create(builder =>
+ {
+ builder.AddConsole();
+ builder.SetMinimumLevel(LogLevel.Information);
+ }).CreateLogger();
+
+ var detector = new WindowsInstallationDetector(detectorLogger);
+ var detectionResult = detector.DetectInstallationsAsync().GetAwaiter().GetResult();
+
+ if (!detectionResult.Success || !detectionResult.Items.Any())
+ {
+ this.logger.LogWarning("Installation detection failed: {Error}", detectionResult.FirstError);
+ return null;
+ }
+
+ // Find the installation that matches the requested game type
+ var matchingInstallation = detectionResult.Items.FirstOrDefault(install =>
+ (gameType == GameType.Generals && install.HasGenerals) ||
+ (gameType == GameType.ZeroHour && install.HasZeroHour));
+
+ if (matchingInstallation == null)
+ {
+ this.logger.LogWarning("No installation found for {GameType}. Available installations:", gameType);
+ foreach (var install in detectionResult.Items)
+ {
+ this.logger.LogWarning(" - {Type}: Generals={HasGenerals} ({GeneralsPath}), ZeroHour={HasZeroHour} ({ZeroHourPath})", install.InstallationType, install.HasGenerals, install.GeneralsPath, install.HasZeroHour, install.ZeroHourPath);
+ }
+ return null;
+ }
+
+ var installationPath = gameType == GameType.Generals
+ ? matchingInstallation.GeneralsPath
+ : matchingInstallation.ZeroHourPath;
+
+ this.logger.LogInformation("Found installation path for {GameType}: {Path}", gameType, installationPath);
+ return installationPath;
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(ex, "Error detecting installation path for {GameType}", gameType);
+ return null;
+ }
+ }
+
+ private async Task> ScanInstallationAsync(string installationPath, GameType gameType)
+ {
+ var entries = new List();
+ var language = this.DetectLanguage(installationPath);
+
+ var files = Directory.GetFiles(installationPath, "*", SearchOption.AllDirectories);
+ var totalFiles = files.Length;
+
+ this.logger.LogInformation("Scanning {Count} files in {Path}", totalFiles, installationPath);
+
+ for (var i = 0; i < files.Length; i++)
+ {
+ var file = files[i];
+ if (i % 100 == 0)
+ {
+ this.logger.LogInformation("Processed {Current}/{Total} files", i, totalFiles);
+ }
+
+ try
+ {
+ var entry = await this.CreateCsvEntryAsync(file, installationPath, gameType, language);
+ if (entry != null)
+ {
+ entries.Add(entry);
+ }
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogWarning(ex, "Failed to process file: {Path}", file);
+ }
+ }
+
+ return entries.OrderBy(e => e.RelativePath).ToList();
+ }
+
+ private async Task CreateCsvEntryAsync(string filePath, string installationPath, GameType gameType, string language)
+ {
+ var relativePath = Path.GetRelativePath(installationPath, filePath).Replace('\\', '/');
+ var fileInfo = new FileInfo(filePath);
+
+ if (fileInfo.Length == 0)
+ {
+ return null; // Skip empty files
+ }
+
+ var (md5, sha256) = await this.CalculateHashesAsync(filePath);
+
+ return new CsvCatalogEntry
+ {
+ RelativePath = relativePath,
+ Size = fileInfo.Length,
+ Md5 = md5,
+ Sha256 = sha256,
+ GameType = gameType.ToString(),
+ Language = language,
+ IsRequired = this.IsRequiredFile(relativePath),
+ Metadata = this.GetFileMetadata(relativePath),
+ };
+ }
+
+ private async Task<(string Md5, string Sha256)> CalculateHashesAsync(string filePath)
+ {
+ using var stream = File.OpenRead(filePath);
+ using var md5 = MD5.Create();
+ using var sha256 = SHA256.Create();
+
+ var buffer = new byte[8192];
+ int bytesRead;
+
+ while ((bytesRead = await stream.ReadAsync(buffer)) > 0)
+ {
+ md5.TransformBlock(buffer, 0, bytesRead, null, 0);
+ sha256.TransformBlock(buffer, 0, bytesRead, null, 0);
+ }
+
+ md5.TransformFinalBlock(Array.Empty(), 0, 0);
+ sha256.TransformFinalBlock(Array.Empty(), 0, 0);
+
+ return (
+ BitConverter.ToString(md5.Hash!).Replace("-", string.Empty).ToLowerInvariant(),
+ BitConverter.ToString(sha256.Hash!).Replace("-", string.Empty).ToLowerInvariant());
+ }
+
+ private string DetectLanguage(string installationPath)
+ {
+ // Check for language-specific files
+ var languageFiles = new[]
+ {
+ ("Data/Lang/English/game.str", "EN"),
+ ("Data/Lang/German/game.str", "DE"),
+ ("Data/Lang/French/game.str", "FR"),
+ ("Data/Lang/Spanish/game.str", "ES"),
+ ("Data/Lang/Italian/game.str", "IT"),
+ ("Data/Lang/Korean/game.str", "KO"),
+ ("Data/Lang/Polish/game.str", "PL"),
+ ("Data/Lang/Portuguese/game.str", "PT-BR"),
+ ("Data/Lang/Chinese_S/game.str", "ZH-CN"),
+ ("Data/Lang/Chinese_T/game.str", "ZH-TW"),
+ };
+
+ foreach (var (file, lang) in languageFiles)
+ {
+ if (File.Exists(Path.Combine(installationPath, file)))
+ {
+ return lang;
+ }
+ }
+
+ return "EN"; // Default to English
+ }
+
+ private bool IsRequiredFile(string relativePath)
+ {
+ // Core game files that are always required
+ var requiredFiles = new[]
+ {
+ "game.exe",
+ "game.dat",
+ "Data/INI/GameData.ini",
+ "Data/INI/English.ini",
+ "Data/Lang/English/game.str",
+ };
+
+ return requiredFiles.Any(rf => relativePath.EndsWith(rf, StringComparison.OrdinalIgnoreCase));
+ }
+
+ private string GetFileMetadata(string relativePath)
+ {
+ var metadata = new Dictionary();
+
+ if (relativePath.StartsWith("Data/INI/", StringComparison.OrdinalIgnoreCase))
+ {
+ metadata["category"] = "config";
+ }
+ else if (relativePath.StartsWith("Data/Lang/", StringComparison.OrdinalIgnoreCase))
+ {
+ metadata["category"] = "language";
+ }
+ else if (relativePath.StartsWith("Data/Maps/", StringComparison.OrdinalIgnoreCase))
+ {
+ metadata["category"] = "maps";
+ }
+ else if (relativePath.EndsWith(".wav", StringComparison.OrdinalIgnoreCase) ||
+ relativePath.EndsWith(".mp3", StringComparison.OrdinalIgnoreCase))
+ {
+ metadata["category"] = "audio";
+ }
+ else if (relativePath.EndsWith(".w3d", StringComparison.OrdinalIgnoreCase) ||
+ relativePath.EndsWith(".dds", StringComparison.OrdinalIgnoreCase))
+ {
+ metadata["category"] = "graphics";
+ }
+ else
+ {
+ metadata["category"] = "other";
+ }
+
+ return JsonConvert.SerializeObject(metadata);
+ }
+
+ private async Task WriteCsvFileAsync(List entries, string csvPath)
+ {
+ var config = new CsvConfiguration(System.Globalization.CultureInfo.InvariantCulture)
+ {
+ HasHeaderRecord = true,
+ };
+
+ await using var writer = new StreamWriter(csvPath);
+ await using var csv = new CsvWriter(writer, config);
+
+ await csv.WriteRecordsAsync(entries);
+ }
+
+ private async Task UpdateIndexJsonAsync()
+ {
+ var registryDir = this.GetRegistryDirectory();
+ var indexPath = Path.Combine(registryDir, "index.json");
+
+ var registries = new List();
+
+ foreach (var csvFile in Directory.GetFiles(registryDir, "*.csv"))
+ {
+ var fileName = Path.GetFileName(csvFile);
+ var gameType = fileName.Contains("Generals") ? GameType.Generals : GameType.ZeroHour;
+ var version = fileName.Contains("1.08") ? "1.08" : "1.04";
+
+ var entries = this.ReadCsvEntriesAsync(csvFile);
+ var (md5, sha256) = await this.CalculateHashesAsync(csvFile);
+
+ registries.Add(new CsvRegistryEntry
+ {
+ Id = $"{gameType.ToString().ToLowerInvariant()}-{version}",
+ GameType = gameType.ToString(),
+ Version = version,
+ Url = $"https://raw.githubusercontent.com/Community-Outpost/GenHub/main/docs/GameInstallationFilesRegistry/{fileName}",
+ FileCount = entries.Count,
+ TotalSizeBytes = entries.Sum(e => e.Size),
+ Languages = entries.Select(e => e.Language).Distinct().OrderBy(l => l).ToList(),
+ Checksum = new Checksum { Md5 = md5, Sha256 = sha256 },
+ GeneratedAt = DateTime.UtcNow.ToString("O"),
+ GeneratorVersion = "1.0.0",
+ IsActive = true,
+ });
+ }
+
+ var index = new CsvRegistryIndex
+ {
+ Version = "1.0.0",
+ LastUpdated = DateTime.UtcNow.ToString("O"),
+ Description = "CSV registry metadata for Command & Conquer Generals and Zero Hour installation validation",
+ Registries = registries,
+ };
+
+ var json = JsonConvert.SerializeObject(index, Formatting.Indented);
+ await File.WriteAllTextAsync(indexPath, json);
+
+ this.logger.LogInformation("Updated index.json with {Count} registries", registries.Count);
+ }
+
+ private List ReadCsvEntriesAsync(string csvPath)
+ {
+ var config = new CsvConfiguration(System.Globalization.CultureInfo.InvariantCulture)
+ {
+ HasHeaderRecord = true,
+ };
+
+ using var reader = new StreamReader(csvPath);
+ using var csv = new CsvReader(reader, config);
+
+ var records = csv.GetRecords();
+ return records.ToList();
+ }
+
+ private string GetRegistryDirectory()
+ {
+ return Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "docs", "GameInstallationFilesRegistry");
+ }
+}
+
+///
+/// Represents a checksum with MD5 and SHA256 hashes.
+///
+internal class Checksum
+{
+ ///
+ /// Gets or sets the MD5 hash.
+ ///
+ public string? Md5 { get; set; }
+
+ ///
+ /// Gets or sets the SHA256 hash.
+ ///
+ public string? Sha256 { get; set; }
+}
+
+///
+/// Represents an entry in the CSV registry index.
+///
+internal class CsvRegistryEntry
+{
+ ///
+ /// Gets or sets the registry ID.
+ ///
+ public string? Id { get; set; }
+
+ ///
+ /// Gets or sets the game type.
+ ///
+ public string? GameType { get; set; }
+
+ ///
+ /// Gets or sets the game version.
+ ///
+ public string? Version { get; set; }
+
+ ///
+ /// Gets or sets the CSV file URL.
+ ///
+ public string? Url { get; set; }
+
+ ///
+ /// Gets or sets the number of files in the CSV.
+ ///
+ public int FileCount { get; set; }
+
+ ///
+ /// Gets or sets the total size of all files in bytes.
+ ///
+ public long TotalSizeBytes { get; set; }
+
+ ///
+ /// Gets or sets the list of supported languages.
+ ///
+ public List? Languages { get; set; }
+
+ ///
+ /// Gets or sets the checksum information.
+ ///
+ public Checksum? Checksum { get; set; }
+
+ ///
+ /// Gets or sets the generation timestamp.
+ ///
+ public string? GeneratedAt { get; set; }
+
+ ///
+ /// Gets or sets the generator version.
+ ///
+ public string? GeneratorVersion { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether this registry is active.
+ ///
+ public bool IsActive { get; set; }
+}
+
+///
+/// Represents the CSV registry index.
+///
+internal class CsvRegistryIndex
+{
+ ///
+ /// Gets or sets the index version.
+ ///
+ public string? Version { get; set; }
+
+ ///
+ /// Gets or sets the last updated timestamp.
+ ///
+ public string? LastUpdated { get; set; }
+
+ ///
+ /// Gets or sets the index description.
+ ///
+ public string? Description { get; set; }
+
+ ///
+ /// Gets or sets the list of registry entries.
+ ///
+ public List? Registries { get; set; }
+}
diff --git a/GenHub/Directory.Packages.props b/GenHub/Directory.Packages.props
index 2b95e10d..7772036f 100644
--- a/GenHub/Directory.Packages.props
+++ b/GenHub/Directory.Packages.props
@@ -13,8 +13,11 @@
+
+
+
@@ -23,6 +26,7 @@
+
@@ -34,4 +38,4 @@
-
\ No newline at end of file
+
diff --git a/GenHub/GenHub.Core/Constants/ConfigurationKeys.cs b/GenHub/GenHub.Core/Constants/ConfigurationKeys.cs
index 4b556327..c5804d9c 100644
--- a/GenHub/GenHub.Core/Constants/ConfigurationKeys.cs
+++ b/GenHub/GenHub.Core/Constants/ConfigurationKeys.cs
@@ -1,5 +1,4 @@
namespace GenHub.Core.Constants;
-
///
/// Configuration key constants for appsettings.json and environment variables.
///
@@ -9,101 +8,99 @@ public static class ConfigurationKeys
/// Base configuration section for GenHub settings.
///
public const string GenHubSection = "GenHub";
-
// Workspace configuration keys
-
///
/// Configuration key for default workspace path.
///
public const string WorkspaceDefaultPath = "GenHub:Workspace:DefaultPath";
-
///
/// Configuration key for default workspace strategy.
///
public const string WorkspaceDefaultStrategy = "GenHub:Workspace:DefaultStrategy";
-
// Cache configuration keys
-
///
/// Configuration key for default cache directory path.
///
public const string CacheDefaultPath = "GenHub:Cache:DefaultPath";
-
// UI configuration keys
-
///
/// Configuration key for default UI theme.
///
public const string UiDefaultTheme = "GenHub:UI:DefaultTheme";
-
///
/// Configuration key for default window width.
///
public const string UiDefaultWindowWidth = "GenHub:UI:DefaultWindowWidth";
-
///
/// Configuration key for default window height.
///
public const string UiDefaultWindowHeight = "GenHub:UI:DefaultWindowHeight";
-
// Downloads configuration keys
-
///
/// Configuration key for default download timeout in seconds.
///
public const string DownloadsDefaultTimeoutSeconds = "GenHub:Downloads:DefaultTimeoutSeconds";
-
///
/// Configuration key for default user agent string.
///
public const string DownloadsDefaultUserAgent = "GenHub:Downloads:DefaultUserAgent";
-
///
/// Configuration key for default maximum concurrent downloads.
///
public const string DownloadsDefaultMaxConcurrent = "GenHub:Downloads:DefaultMaxConcurrent";
-
///
/// Configuration key for default download buffer size.
///
public const string DownloadsDefaultBufferSize = "GenHub:Downloads:DefaultBufferSize";
-
// Downloads policy configuration keys
-
///
/// Configuration key for minimum concurrent downloads policy.
///
public const string DownloadsPolicyMinConcurrent = "GenHub:Downloads:Policy:MinConcurrent";
-
///
/// Configuration key for maximum concurrent downloads policy.
///
public const string DownloadsPolicyMaxConcurrent = "GenHub:Downloads:Policy:MaxConcurrent";
-
///
/// Configuration key for minimum download timeout policy.
///
public const string DownloadsPolicyMinTimeoutSeconds = "GenHub:Downloads:Policy:MinTimeoutSeconds";
-
///
/// Configuration key for maximum download timeout policy.
///
public const string DownloadsPolicyMaxTimeoutSeconds = "GenHub:Downloads:Policy:MaxTimeoutSeconds";
-
///
/// Configuration key for minimum download buffer size policy.
///
public const string DownloadsPolicyMinBufferSizeBytes = "GenHub:Downloads:Policy:MinBufferSizeBytes";
-
///
/// Configuration key for maximum download buffer size policy.
///
public const string DownloadsPolicyMaxBufferSizeBytes = "GenHub:Downloads:Policy:MaxBufferSizeBytes";
-
// App data configuration key
-
///
/// Configuration key for application data path.
///
public const string AppDataPath = "GenHub:AppDataPath";
+ // CSV configuration keys
+ ///
+ /// Configuration key for CSV index URL.
+ ///
+ public const string CsvIndexUrl = "GenHub:CSV:IndexUrl";
+ ///
+ /// Configuration key for CSV catalogs fallback.
+ ///
+ public const string CsvCatalogs = "GenHub:CSV:Catalogs";
+ ///
+ /// Configuration key for auto-detect language flag.
+ ///
+ public const string CsvAutoDetectLanguage = "GenHub:CSV:AutoDetectLanguage";
+ ///
+ /// Configuration key for CSV cache TTL in minutes.
+ ///
+ public const string CsvCacheTtlMinutes = "GenHub:CSV:CacheTtlMinutes";
+ ///
+ /// Configuration key for CSV strict mode.
+ ///
+ public const string CsvStrictMode = "GenHub:CSV:StrictMode";
}
diff --git a/GenHub/GenHub.Core/Constants/CsvConstants.cs b/GenHub/GenHub.Core/Constants/CsvConstants.cs
new file mode 100644
index 00000000..5b4df93d
--- /dev/null
+++ b/GenHub/GenHub.Core/Constants/CsvConstants.cs
@@ -0,0 +1,151 @@
+using System;
+
+namespace GenHub.Core.Constants;
+
+/// Constants used for CSV file processing and content handling.
+public static class CsvConstants
+{
+ /// CSV file extension.
+ public const string CsvFileExtension = ".csv";
+
+ /// CSV file pattern for file matching.
+ public const string CsvFilePattern = "*.csv";
+
+ /// Default CSV delimiter character.
+ public const char CsvDelimiter = ',';
+
+ /// Maximum number of columns expected in CSV files.
+ public const int MaxCsvColumns = 8;
+
+ /// Minimum number of columns required in CSV files.
+ public const int MinCsvColumns = 4;
+
+ /// Column index for relative path in CSV files (0-based).
+ public const int RelativePathColumnIndex = 0;
+
+ /// Column index for file size in CSV files (0-based).
+ public const int SizeColumnIndex = 1;
+
+ /// Column index for MD5 hash in CSV files (0-based).
+ public const int Md5ColumnIndex = 2;
+
+ /// Column index for SHA256 hash in CSV files (0-based).
+ public const int Sha256ColumnIndex = 3;
+
+ /// Column index for game type in CSV files (0-based).
+ public const int GameTypeColumnIndex = 4;
+
+ /// Column index for language in CSV files (0-based).
+ public const int LanguageColumnIndex = 5;
+
+ /// Column index for isRequired in CSV files (0-based).
+ public const int IsRequiredColumnIndex = 6;
+
+ /// Column index for metadata in CSV files (0-based).
+ public const int MetadataColumnIndex = 7;
+
+ /// Default buffer size for CSV file reading operations.
+ public const int DefaultCsvBufferSize = 8192;
+
+ /// Timeout for CSV download operations in seconds.
+ public const int CsvDownloadTimeoutSeconds = 30;
+
+ /// Maximum retry attempts for CSV operations.
+ public const int MaxCsvRetryAttempts = 3;
+
+ /// Delay between CSV retry attempts in milliseconds.
+ public const int CsvRetryDelayMs = 1000;
+
+ /// Default CSV URL for Generals content.
+ public const string DefaultGeneralsCsvUrl = "https://raw.githubusercontent.com/Community-Outpost/GenHub/main/docs/GameInstallationFilesRegistry/Generals-1.08.csv";
+
+ /// Default CSV URL for Zero Hour content.
+ public const string DefaultZeroHourCsvUrl = "https://raw.githubusercontent.com/Community-Outpost/GenHub/main/docs/GameInstallationFilesRegistry/ZeroHour-1.04.csv";
+
+ /// Default index.json URL for CSV registry metadata.
+ public const string DefaultCsvIndexUrl = "https://raw.githubusercontent.com/Community-Outpost/GenHub/main/docs/GameInstallationFilesRegistry/index.json";
+
+ /// CSV resolver identifier.
+ public const string CsvResolverId = "CSVResolver";
+
+ /// CSV source name identifier.
+ public const string CsvSourceName = "CSV";
+
+ /// File system deliverer source name.
+ public const string FileSystemSourceName = "FileSystem";
+
+ /// Default hash algorithm to use for file validation.
+ public const string DefaultHashAlgorithm = "SHA256";
+
+ /// Progress message for CSV download phase.
+ public const string DownloadingCsvMessage = "Downloading CSV file...";
+
+ /// Progress message for CSV parsing and validation phase.
+ public const string ParsingCsvMessage = "Parsing CSV and validating files...";
+
+ /// Error message when CSV URL is not provided.
+ public const string CsvUrlNotProvidedError = "CSV URL not provided by discoverer";
+
+ /// Error message when CSV download fails.
+ public const string CsvDownloadFailedError = "Failed to download CSV";
+
+ /// Error message when content is not found.
+ public const string ContentNotFoundError = "Content not found";
+
+ /// Error message when manifest is not available.
+ public const string ManifestNotAvailableError = "Manifest not available in search result";
+
+ /// Error message for operation cancellation.
+ public const string OperationCancelledError = "Operation canceled";
+
+ /// Error message for CSV resolve failure.
+ public const string CsvResolveFailedError = "CSV resolve failed";
+
+ /// Error message for CSV content preparation failure.
+ public const string CsvPreparationFailedError = "CSV content preparation failed";
+
+ /// Warning message for malformed CSV lines.
+ public const string MalformedCsvLineWarning = "CSVDeliverer: skipping malformed CSV line {LineIndex} (not enough columns)";
+
+ /// Warning message for empty relative paths.
+ public const string EmptyRelativePathWarning = "CSVDeliverer: skipping empty relPath at line {LineIndex}";
+
+ /// Warning message for size parse failures.
+ public const string SizeParseFailedWarning = "CSVDeliverer: skipping {RelPath} because size parse failed at line {LineIndex}";
+
+ /// Warning message for MD5 mismatches.
+ public const string Md5MismatchWarning = "MD5 mismatch: {RelativePath}";
+
+ /// Warning message for SHA256 mismatches.
+ public const string Sha256MismatchWarning = "SHA256 mismatch: {RelativePath}";
+
+ /// Warning message for missing files.
+ public const string FileMissingWarning = "File missing: {RelativePath}";
+
+ /// Warning message for invalid file sizes.
+ public const string InvalidSizeWarning = "Invalid size for file {RelativePath}";
+
+ /// Debug message for starting CSV preparation.
+ public const string StartingCsvPreparationDebug = "Starting CSV content preparation for manifest {ManifestId}";
+
+ /// Debug message for successful CSV preparation completion.
+ public const string CsvPreparationCompletedDebug = "CSV content preparation completed successfully for manifest {ManifestId}";
+
+ /// Warning message for CSV validation errors.
+ public const string CsvValidationErrorsWarning = "CSV validation completed with errors: {Errors}";
+
+ /// Error message for CSV download failures.
+ public const string CsvDownloadFailedLog = "CSVDeliverer: failed to download CSV from {Url} - status {Status}";
+
+ /// Error message for missing CSV URL.
+ public const string CsvUrlMissingLog = "CSVDeliverer: csvUrl not provided in discovered item (Id={ContentId})";
+
+ /// Warning message for operation cancellation.
+ public const string OperationCancelledLog = "CSVDeliverer: operation was cancelled";
+
+ /// Error message for CSV resolve failures.
+ public const string CsvResolveFailedLog = "CSVDeliverer: failed to resolve CSV discovered item (Id={ContentId})";
+
+ /// Error message for CSV preparation failures.
+ public const string CsvPreparationFailedLog = "CSV content preparation failed for manifest {ManifestId}";
+}
diff --git a/GenHub/GenHub.Core/Extensions/GameInstallations/InstallationExtensions.cs b/GenHub/GenHub.Core/Extensions/GameInstallations/InstallationExtensions.cs
index a2c941c7..3ce3becf 100644
--- a/GenHub/GenHub.Core/Extensions/GameInstallations/InstallationExtensions.cs
+++ b/GenHub/GenHub.Core/Extensions/GameInstallations/InstallationExtensions.cs
@@ -20,15 +20,15 @@ public static GameInstallation ToDomain(this IGameInstallation installation, ILo
{
logger?.LogDebug("Converting {InstallationType} installation to domain model", installation.InstallationType);
- var installationPath = installation.HasGenerals ? installation.GeneralsPath : installation.ZeroHourPath;
- if (string.IsNullOrEmpty(installationPath))
+ var gameInstallation = new GameInstallation(installation.InstallationPath, installation.InstallationType, logger as ILogger)
{
- installationPath = installation.InstallationPath;
- }
-
- var gameInstallation = new GameInstallation(installationPath, installation.InstallationType, logger as ILogger);
+ HasGenerals = installation.HasGenerals,
+ GeneralsPath = installation.GeneralsPath,
+ HasZeroHour = installation.HasZeroHour,
+ ZeroHourPath = installation.ZeroHourPath,
+ };
- logger?.LogDebug("Successfully converted installation to domain model: {InstallationPath}", installationPath);
+ logger?.LogDebug("Successfully converted installation to domain model: {InstallationPath}", installation.InstallationPath);
return gameInstallation;
}
diff --git a/GenHub/GenHub.Core/Features/GameInstallations/ILanguageDetector.cs b/GenHub/GenHub.Core/Features/GameInstallations/ILanguageDetector.cs
new file mode 100644
index 00000000..d99c7b6d
--- /dev/null
+++ b/GenHub/GenHub.Core/Features/GameInstallations/ILanguageDetector.cs
@@ -0,0 +1,18 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace GenHub.Core.Features.GameInstallations;
+
+///
+/// Interface for detecting the language of a game installation.
+///
+public interface ILanguageDetector
+{
+ ///
+ /// Detects the language of a game installation at the specified path.
+ ///
+ /// The path to the game installation directory.
+ /// A token to cancel the operation.
+ /// The detected language code in uppercase (e.g., "EN", "DE"), or "EN" as fallback.
+ Task DetectAsync(string installationPath, CancellationToken cancellationToken = default);
+}
diff --git a/GenHub/GenHub.Core/Features/GameInstallations/LanguageDetector.cs b/GenHub/GenHub.Core/Features/GameInstallations/LanguageDetector.cs
new file mode 100644
index 00000000..496a4c86
--- /dev/null
+++ b/GenHub/GenHub.Core/Features/GameInstallations/LanguageDetector.cs
@@ -0,0 +1,143 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace GenHub.Core.Features.GameInstallations;
+
+///
+/// Detects the language of a Command & Conquer Generals or Zero Hour installation.
+///
+public class LanguageDetector : ILanguageDetector
+{
+ ///
+ /// Detects the language of a game installation at the specified path.
+ ///
+ /// The path to the game installation directory.
+ /// A token to cancel the operation.
+ /// The detected language code in uppercase (e.g., "EN", "DE"), or "EN" as fallback.
+ public Task DetectAsync(string installationPath, CancellationToken cancellationToken = default)
+ {
+ if (!Directory.Exists(installationPath))
+ {
+ return Task.FromResult("EN"); // Fallback
+ }
+
+ // Check for language-specific directories and files
+ var languageMappings = new[]
+ {
+ new { Pattern = "Data\\english", Language = "EN" },
+ new { Pattern = "Data\\English", Language = "EN" },
+ new { Pattern = "Data\\german", Language = "DE" },
+ new { Pattern = "Data\\deutsch", Language = "DE" },
+ new { Pattern = "Data\\french", Language = "FR" },
+ new { Pattern = "Data\\spanish", Language = "ES" },
+ new { Pattern = "Data\\italian", Language = "IT" },
+ new { Pattern = "Data\\korean", Language = "KO" },
+ new { Pattern = "Data\\polish", Language = "PL" },
+ new { Pattern = "Data\\portuguese", Language = "PT-BR" },
+ new { Pattern = "Data\\chinese", Language = "ZH-CN" },
+ new { Pattern = "Data\\chinese-traditional", Language = "ZH-TW" },
+ };
+
+ foreach (var mapping in languageMappings)
+ {
+ if (Directory.Exists(Path.Combine(installationPath, mapping.Pattern)))
+ {
+ return Task.FromResult(mapping.Language);
+ }
+ }
+
+ // Check for language-specific files
+ var fileMappings = new[]
+ {
+ // English
+ new { Pattern = "English.big", Language = "EN" },
+ new { Pattern = "AudioEnglish.big", Language = "EN" },
+ new { Pattern = "SpeechEnglish.big", Language = "EN" },
+
+ // German
+ new { Pattern = "German.big", Language = "DE" },
+ new { Pattern = "AudioGerman.big", Language = "DE" },
+
+ // French
+ new { Pattern = "French.big", Language = "FR" },
+ new { Pattern = "AudioFrench.big", Language = "FR" },
+
+ // Spanish
+ new { Pattern = "Spanish.big", Language = "ES" },
+ new { Pattern = "AudioSpanish.big", Language = "ES" },
+
+ // Italian
+ new { Pattern = "Italian.big", Language = "IT" },
+ new { Pattern = "AudioItalian.big", Language = "IT" },
+
+ // Korean
+ new { Pattern = "Korean.big", Language = "KO" },
+ new { Pattern = "AudioKorean.big", Language = "KO" },
+
+ // Polish
+ new { Pattern = "Polish.big", Language = "PL" },
+ new { Pattern = "AudioPolish.big", Language = "PL" },
+
+ // Portuguese-Brazil
+ new { Pattern = "PortugueseBrazil.big", Language = "PT-BR" },
+ new { Pattern = "AudioPortugueseBrazil.big", Language = "PT-BR" },
+
+ // Chinese Simplified
+ new { Pattern = "Chinese.big", Language = "ZH-CN" },
+ new { Pattern = "AudioChinese.big", Language = "ZH-CN" },
+
+ // Chinese Traditional
+ new { Pattern = "ChineseTraditional.big", Language = "ZH-TW" },
+ new { Pattern = "AudioChineseTraditional.big", Language = "ZH-TW" },
+ };
+
+ foreach (var mapping in fileMappings)
+ {
+ if (File.Exists(Path.Combine(installationPath, mapping.Pattern)))
+ {
+ return Task.FromResult(mapping.Language);
+ }
+ }
+
+ // Check for Zero Hour specific patterns
+ var zhPatterns = new[]
+ {
+ new { Pattern = "EnglishZH.big", Language = "EN" },
+ new { Pattern = "AudioZH.big", Language = "EN" },
+ new { Pattern = "INIZH.big", Language = "EN" },
+ new { Pattern = "*ZH.big", Language = "EN" }, // Generic ZH files
+ new { Pattern = "GeneralsOnlineZH", Language = "EN" }, // Executables
+ new { Pattern = "GermanZH.big", Language = "DE" },
+ new { Pattern = "FrenchZH.big", Language = "FR" },
+ new { Pattern = "SpanishZH.big", Language = "ES" },
+ new { Pattern = "ItalianZH.big", Language = "IT" },
+ new { Pattern = "KoreanZH.big", Language = "KO" },
+ new { Pattern = "PolishZH.big", Language = "PL" },
+ new { Pattern = "PortugueseZH.big", Language = "PT-BR" },
+ new { Pattern = "ChineseZH.big", Language = "ZH-CN" },
+ };
+
+ foreach (var mapping in zhPatterns)
+ {
+ if (mapping.Pattern.Contains("*"))
+ {
+ // Handle wildcard
+ var files = Directory.GetFiles(installationPath, mapping.Pattern, SearchOption.AllDirectories);
+ if (files.Length > 0)
+ {
+ return Task.FromResult(mapping.Language);
+ }
+ }
+ else if (File.Exists(Path.Combine(installationPath, mapping.Pattern)))
+ {
+ return Task.FromResult(mapping.Language);
+ }
+ }
+
+ // Fallback to English
+ return Task.FromResult("EN");
+ }
+}
diff --git a/GenHub/GenHub.Core/GenHub.Core.csproj b/GenHub/GenHub.Core/GenHub.Core.csproj
index 3fa719d5..de26dc4b 100644
--- a/GenHub/GenHub.Core/GenHub.Core.csproj
+++ b/GenHub/GenHub.Core/GenHub.Core.csproj
@@ -10,5 +10,6 @@
+
diff --git a/GenHub/GenHub.Core/Models/Content/ContentSearchQuery.cs b/GenHub/GenHub.Core/Models/Content/ContentSearchQuery.cs
index f58b49a3..3d451351 100644
--- a/GenHub/GenHub.Core/Models/Content/ContentSearchQuery.cs
+++ b/GenHub/GenHub.Core/Models/Content/ContentSearchQuery.cs
@@ -2,13 +2,13 @@
using System.Collections.Generic;
using GenHub.Core.Models.Enums;
-namespace GenHub.Core.Models.Content;
-
///
/// Represents a query for searching content across providers.
///
public class ContentSearchQuery
{
+ private string? _language;
+
///
/// Gets or sets the primary search term.
///
@@ -78,4 +78,47 @@ public class ContentSearchQuery
/// Gets or sets sort value.
///
public string Sort { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the optional language filter used by CSV content pipeline.
+ ///
+ ///
+ /// Accepts case-insensitive input and is normalized to uppercase to match CSV schema values.
+ /// Values: "All", "EN", "DE", "FR", "ES", "IT", "KO", "PL", "PT-BR", "ZH-CN", "ZH-TW".
+ ///
+ public string? Language
+ {
+ get => _language;
+ set => _language = NormalizeLanguage(value);
+ }
+
+ private static string? NormalizeLanguage(string? language)
+ {
+ if (string.IsNullOrWhiteSpace(language))
+ {
+ return language;
+ }
+
+ // Trim and replace underscores with hyphens
+ var normalized = language.Trim().Replace('_', '-');
+
+ // Convert to uppercase
+ normalized = normalized.ToUpperInvariant();
+
+ // Special case for "ALL" to "All"
+ if (normalized == "ALL")
+ {
+ return "All";
+ }
+
+ // Map variants
+ return normalized switch
+ {
+ "PT" => "PT-BR",
+ "ZH" => "ZH-CN",
+ "ZHTW" or "ZH-TW" => "ZH-TW",
+ "ZHCN" or "ZH-CN" => "ZH-CN",
+ _ => normalized,
+ };
+ }
}
diff --git a/GenHub/GenHub.Core/Models/Content/CsvCatalogEntry.cs b/GenHub/GenHub.Core/Models/Content/CsvCatalogEntry.cs
new file mode 100644
index 00000000..4f554006
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Content/CsvCatalogEntry.cs
@@ -0,0 +1,57 @@
+using CsvHelper.Configuration.Attributes;
+
+namespace GenHub.Core.Models.Content;
+
+///
+/// Represents a single entry in the CSV catalog for game installation files.
+///
+public sealed class CsvCatalogEntry
+{
+ ///
+ /// Gets or sets the relative path of the file from the game installation root.
+ ///
+ [Name("relativePath")]
+ public string RelativePath { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the size of the file in bytes.
+ ///
+ [Name("size")]
+ public long Size { get; set; }
+
+ ///
+ /// Gets or sets the MD5 hash of the file.
+ ///
+ [Name("md5")]
+ public string Md5 { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the SHA256 hash of the file.
+ ///
+ [Name("sha256")]
+ public string Sha256 { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the game type (Generals or ZeroHour).
+ ///
+ [Name("gameType")]
+ public string GameType { get; set; } = "Generals";
+
+ ///
+ /// Gets or sets the language code for the file.
+ ///
+ [Name("language")]
+ public string Language { get; set; } = "All";
+
+ ///
+ /// Gets or sets a value indicating whether the file is required for validation.
+ ///
+ [Name("isRequired")]
+ public bool IsRequired { get; set; } = true;
+
+ ///
+ /// Gets or sets additional metadata for the file as JSON string.
+ ///
+ [Name("metadata")]
+ public string? Metadata { get; set; }
+}
diff --git a/GenHub/GenHub.Core/Models/Content/CsvRegistryIndex.cs b/GenHub/GenHub.Core/Models/Content/CsvRegistryIndex.cs
new file mode 100644
index 00000000..cd8bb70a
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Content/CsvRegistryIndex.cs
@@ -0,0 +1,106 @@
+using System.Collections.Generic;
+
+namespace GenHub.Core.Models.Content;
+
+///
+/// Represents the CSV registry index containing metadata about available CSV files.
+///
+public class CsvRegistryIndex
+{
+ ///
+ /// Gets or sets the version of the index format.
+ ///
+ public string Version { get; set; } = "1.0.0";
+
+ ///
+ /// Gets or sets the last updated timestamp in ISO 8601 format.
+ ///
+ public string LastUpdated { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the description of the registry.
+ ///
+ public string Description { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the list of available registries.
+ ///
+ public List Registries { get; set; } = new();
+}
+
+///
+/// Represents a single registry entry in the CSV index.
+///
+public class CsvRegistryEntry
+{
+ ///
+ /// Gets or sets the unique identifier for the registry.
+ ///
+ public string Id { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the game type (Generals or ZeroHour).
+ ///
+ public string GameType { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the game version.
+ ///
+ public string Version { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the URL to the CSV file.
+ ///
+ public string Url { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the number of files in the CSV.
+ ///
+ public int FileCount { get; set; }
+
+ ///
+ /// Gets or sets the total size of all files in bytes.
+ ///
+ public long TotalSizeBytes { get; set; }
+
+ ///
+ /// Gets or sets the list of supported languages.
+ ///
+ public List Languages { get; set; } = new();
+
+ ///
+ /// Gets or sets the checksum information.
+ ///
+ public CsvChecksum Checksum { get; set; } = new();
+
+ ///
+ /// Gets or sets the generation timestamp in ISO 8601 format.
+ ///
+ public string GeneratedAt { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the generator version.
+ ///
+ public string GeneratorVersion { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets a value indicating whether this registry is active.
+ ///
+ public bool IsActive { get; set; } = true;
+}
+
+///
+/// Represents checksum information for a CSV file.
+///
+public class CsvChecksum
+{
+ ///
+ /// Gets or sets the MD5 checksum.
+ ///
+ public string Md5 { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the SHA256 checksum.
+ ///
+ public string Sha256 { get; set; } = string.Empty;
+}
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Content/CSVContentProviderTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Content/CSVContentProviderTests.cs
new file mode 100644
index 00000000..785c045d
--- /dev/null
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Content/CSVContentProviderTests.cs
@@ -0,0 +1,190 @@
+using GenHub.Core.Constants;
+using GenHub.Core.Interfaces.Content;
+using GenHub.Core.Models.Content;
+using GenHub.Core.Models.Enums;
+using GenHub.Core.Models.Manifest;
+using GenHub.Core.Models.Results;
+using GenHub.Features.Content.Services.ContentProviders;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Xunit;
+
+namespace GenHub.Tests.Core.Features.Content;
+
+///
+/// Unit tests for .
+///
+public class CsvContentProviderTests
+{
+ private readonly Mock _discovererMock;
+ private readonly Mock _resolverMock;
+ private readonly Mock _delivererMock;
+ private readonly Mock> _loggerMock;
+ private readonly Mock _contentValidatorMock;
+ private readonly HttpClient _httpClient;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CsvContentProviderTests()
+ {
+ _discovererMock = new Mock();
+ _discovererMock.Setup(d => d.SourceName).Returns(CsvConstants.CsvSourceName);
+
+ _resolverMock = new Mock();
+ _resolverMock.Setup(r => r.ResolverId).Returns(CsvConstants.CsvResolverId);
+
+ _delivererMock = new Mock();
+ _delivererMock.Setup(d => d.SourceName).Returns(CsvConstants.FileSystemSourceName);
+
+ _loggerMock = new Mock>();
+ _contentValidatorMock = new Mock();
+ _httpClient = new HttpClient();
+ }
+
+ ///
+ /// Verifies that the constructor sets up the provider correctly.
+ ///
+ [Fact]
+ public void Constructor_WithValidParameters_SetsUpProviderCorrectly()
+ {
+ // Arrange
+ var discoverers = new[] { _discovererMock.Object };
+ var resolvers = new[] { _resolverMock.Object };
+ var deliverers = new[] { _delivererMock.Object };
+ var csvUrl = "https://example.com/test.csv";
+
+ // Act
+ var provider = new CsvContentProvider(
+ discoverers,
+ resolvers,
+ deliverers,
+ _loggerMock.Object,
+ _contentValidatorMock.Object,
+ csvUrl);
+
+ // Assert
+ Assert.Equal("CSV", provider.SourceName);
+ Assert.Equal("Content Provider backed by authoritative CSV catalog", provider.Description);
+ Assert.True(provider.IsEnabled);
+ Assert.Equal(ContentSourceCapabilities.RequiresDiscovery | ContentSourceCapabilities.SupportsManifestGeneration | ContentSourceCapabilities.LocalFileDelivery, provider.Capabilities);
+ }
+
+ ///
+ /// Verifies that SearchAsync calls the base implementation.
+ ///
+ /// A task representing the asynchronous operation.
+ [Fact]
+ public async Task SearchAsync_WithValidQuery_CallsBaseImplementation()
+ {
+ // Arrange
+ var discoverers = new[] { _discovererMock.Object };
+ var resolvers = new[] { _resolverMock.Object };
+ var deliverers = new[] { _delivererMock.Object };
+ var csvUrl = "https://example.com/test.csv";
+
+ _discovererMock.Setup(d => d.DiscoverAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(OperationResult>.CreateSuccess(Enumerable.Empty()));
+
+ var provider = new CsvContentProvider(
+ discoverers,
+ resolvers,
+ deliverers,
+ _loggerMock.Object,
+ _contentValidatorMock.Object,
+ csvUrl);
+
+ var query = new ContentSearchQuery { TargetGame = GameType.Generals };
+
+ // Act
+ var result = await provider.SearchAsync(query);
+
+ // Assert
+ Assert.NotNull(result);
+ _discovererMock.Verify(d => d.DiscoverAsync(It.IsAny(), It.IsAny()), Times.Once);
+ }
+
+ ///
+ /// Verifies that GetValidatedContentAsync returns content for valid ID.
+ ///
+ /// A task representing the asynchronous operation.
+ [Fact]
+ public async Task GetValidatedContentAsync_WithValidContentId_ReturnsContent()
+ {
+ // Arrange
+ var discoverers = new[] { _discovererMock.Object };
+ var resolvers = new[] { _resolverMock.Object };
+ var deliverers = new[] { _delivererMock.Object };
+ var csvUrl = "https://example.com/test.csv";
+
+ var mockSearchResult = new ContentSearchResult
+ {
+ Id = "test-content-id",
+ Name = "Test Content",
+ ProviderName = CsvConstants.CsvSourceName,
+ };
+ mockSearchResult.SetData(new ContentManifest
+ {
+ Id = "1.0.csv.generals-content.test-content",
+ Name = "Test Content",
+ Files = new List(),
+ });
+
+ _discovererMock.Setup(d => d.DiscoverAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(OperationResult>.CreateSuccess(new[] { mockSearchResult }));
+
+ var provider = new CsvContentProvider(
+ discoverers,
+ resolvers,
+ deliverers,
+ _loggerMock.Object,
+ _contentValidatorMock.Object,
+ csvUrl);
+
+ var contentId = "test-content-id";
+
+ // Act
+ var result = await provider.GetValidatedContentAsync(contentId);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.True(result.Success);
+ }
+
+ ///
+ /// Verifies that PrepareContentAsync processes content correctly.
+ ///
+ /// A task representing the asynchronous operation.
+ [Fact]
+ public async Task PrepareContentAsync_WithValidManifest_ProcessesContent()
+ {
+ // Arrange
+ var discoverers = new[] { _discovererMock.Object };
+ var resolvers = new[] { _resolverMock.Object };
+ var deliverers = new[] { _delivererMock.Object };
+ var csvUrl = "https://example.com/test.csv";
+
+ var provider = new CsvContentProvider(
+ discoverers,
+ resolvers,
+ deliverers,
+ _loggerMock.Object,
+ _contentValidatorMock.Object,
+ csvUrl);
+
+ var manifest = new ContentManifest
+ {
+ Id = "1.0.csv.generals-content.test-manifest",
+ Name = "Test Manifest",
+ Files = new List(),
+ };
+
+ var workingDirectory = Path.GetTempPath();
+
+ // Act
+ var result = await provider.PrepareContentAsync(manifest, workingDirectory);
+
+ // Assert
+ Assert.NotNull(result);
+ }
+}
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Content/CSVDiscovererTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Content/CSVDiscovererTests.cs
new file mode 100644
index 00000000..869c5a0b
--- /dev/null
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Content/CSVDiscovererTests.cs
@@ -0,0 +1,179 @@
+using GenHub.Core.Constants;
+using GenHub.Core.Interfaces.Common;
+using GenHub.Core.Models.Content;
+using GenHub.Core.Models.Enums;
+using GenHub.Features.Content.Services.ContentDiscoverers;
+using Microsoft.Extensions.Logging;
+using Moq;
+
+namespace GenHub.Tests.Core.Features.Content;
+
+///
+/// Unit tests for .
+///
+public class CSVDiscovererTests
+{
+ private readonly Mock> _loggerMock;
+ private readonly Mock _configurationProviderMock;
+ private readonly HttpClient _httpClient;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CSVDiscovererTests()
+ {
+ _loggerMock = new Mock>();
+ _configurationProviderMock = new Mock();
+ _httpClient = new HttpClient();
+ }
+
+ ///
+ /// Verifies that DiscoverAsync returns Generals results when TargetGame is Generals.
+ ///
+ /// A task representing the asynchronous operation.
+ [Fact]
+ public async Task DiscoverAsync_WithGeneralsQuery_ReturnsGeneralsResult()
+ {
+ // Arrange
+ var discoverer = new CSVDiscoverer(
+ _loggerMock.Object,
+ _configurationProviderMock.Object,
+ _httpClient);
+
+ var query = new ContentSearchQuery { TargetGame = GameType.Generals };
+
+ // Act
+ var result = await discoverer.DiscoverAsync(query);
+
+ // Assert
+ Assert.True(result.Success);
+ Assert.NotNull(result.Data);
+ Assert.Single(result.Data);
+ var searchResult = result.Data.First();
+ Assert.Equal("Generals-1.08-All", searchResult.Id);
+ Assert.Equal("Command & Conquer Generals 1.08 (All)", searchResult.Name);
+ Assert.Equal(CsvConstants.CsvResolverId, searchResult.ResolverId);
+ Assert.Equal("Generals", searchResult.ResolverMetadata["game"]);
+ Assert.Equal("1.08", searchResult.ResolverMetadata["version"]);
+ Assert.Equal("All", searchResult.ResolverMetadata["language"]);
+ Assert.Equal(CsvConstants.DefaultGeneralsCsvUrl, searchResult.ResolverMetadata["csvUrl"]);
+ }
+
+ ///
+ /// Verifies that DiscoverAsync returns Zero Hour results when TargetGame is ZeroHour.
+ ///
+ /// A task representing the asynchronous operation.
+ [Fact]
+ public async Task DiscoverAsync_WithZeroHourQuery_ReturnsZeroHourResult()
+ {
+ // Arrange
+ var discoverer = new CSVDiscoverer(
+ _loggerMock.Object,
+ _configurationProviderMock.Object,
+ _httpClient);
+
+ var query = new ContentSearchQuery { TargetGame = GameType.ZeroHour };
+
+ // Act
+ var result = await discoverer.DiscoverAsync(query);
+
+ // Assert
+ Assert.True(result.Success);
+ Assert.NotNull(result.Data);
+ Assert.Single(result.Data);
+ var searchResult = result.Data.First();
+ Assert.Equal("ZeroHour-1.04-All", searchResult.Id);
+ Assert.Equal("Command & Conquer Generals: Zero Hour 1.04 (All)", searchResult.Name);
+ Assert.Equal(CsvConstants.CsvResolverId, searchResult.ResolverId);
+ Assert.Equal("ZeroHour", searchResult.ResolverMetadata["game"]);
+ Assert.Equal("1.04", searchResult.ResolverMetadata["version"]);
+ Assert.Equal("All", searchResult.ResolverMetadata["language"]);
+ Assert.Equal(CsvConstants.DefaultZeroHourCsvUrl, searchResult.ResolverMetadata["csvUrl"]);
+ }
+
+ ///
+ /// Verifies that DiscoverAsync normalizes language to uppercase.
+ ///
+ /// A task representing the asynchronous operation.
+ [Fact]
+ public async Task DiscoverAsync_WithLowercaseLanguage_NormalizesToUppercase()
+ {
+ // Arrange
+ var discoverer = new CSVDiscoverer(
+ _loggerMock.Object,
+ _configurationProviderMock.Object,
+ _httpClient);
+
+ var query = new ContentSearchQuery
+ {
+ TargetGame = GameType.Generals,
+ Language = "en",
+ };
+
+ // Act
+ var result = await discoverer.DiscoverAsync(query);
+
+ // Assert
+ Assert.True(result.Success);
+ Assert.NotNull(result.Data);
+ var searchResult = result.Data.First();
+ Assert.Equal("Generals-1.08-EN", searchResult.Id);
+ Assert.Equal("Command & Conquer Generals 1.08 (EN)", searchResult.Name);
+ Assert.Equal("EN", searchResult.ResolverMetadata["language"]);
+ }
+
+ ///
+ /// Verifies that DiscoverAsync uses "All" as default when language is null.
+ ///
+ /// A task representing the asynchronous operation.
+ [Fact]
+ public async Task DiscoverAsync_WithNullLanguage_UsesAll()
+ {
+ // Arrange
+ var discoverer = new CSVDiscoverer(
+ _loggerMock.Object,
+ _configurationProviderMock.Object,
+ _httpClient);
+
+ var query = new ContentSearchQuery
+ {
+ TargetGame = GameType.Generals,
+ Language = null,
+ };
+
+ // Act
+ var result = await discoverer.DiscoverAsync(query);
+
+ // Assert
+ Assert.True(result.Success);
+ Assert.NotNull(result.Data);
+ var searchResult = result.Data.First();
+ Assert.Equal("All", searchResult.ResolverMetadata["language"]);
+ }
+
+ ///
+ /// Verifies that SourceName returns the correct value.
+ ///
+ [Fact]
+ public void SourceName_ReturnsCorrectValue()
+ {
+ // Arrange
+ var discoverer = new CSVDiscoverer(
+ _loggerMock.Object,
+ _configurationProviderMock.Object,
+ _httpClient);
+
+ // Assert
+ Assert.Equal(CsvConstants.CsvSourceName, discoverer.SourceName);
+ }
+
+ ///
+ /// Verifies that ResolverId returns the correct value.
+ ///
+ [Fact]
+ public void ResolverId_ReturnsCorrectValue()
+ {
+ // Assert
+ Assert.Equal(CsvConstants.CsvResolverId, CSVDiscoverer.ResolverId);
+ }
+}
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Content/CSVResolverTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Content/CSVResolverTests.cs
new file mode 100644
index 00000000..43c4ac35
--- /dev/null
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Content/CSVResolverTests.cs
@@ -0,0 +1,172 @@
+using GenHub.Core.Constants;
+using GenHub.Core.Models.Content;
+using GenHub.Core.Models.Enums;
+using GenHub.Core.Models.Manifest;
+using GenHub.Core.Models.Results;
+using GenHub.Features.Content.Services.ContentResolvers;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Moq.Protected;
+using System.Net;
+namespace GenHub.Tests.Core.Features.Content;
+
+///
+/// Unit tests for .
+///
+public class CSVResolverTests
+{
+ private readonly Mock _httpMessageHandlerMock;
+ private readonly HttpClient _httpClient;
+ private readonly Mock> _loggerMock;
+ private readonly CSVResolver _resolver;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CSVResolverTests()
+ {
+ _httpMessageHandlerMock = new Mock();
+ _httpClient = new HttpClient(_httpMessageHandlerMock.Object);
+ _loggerMock = new Mock>();
+ _resolver = new CSVResolver(_httpClient, _loggerMock.Object);
+ }
+
+ ///
+ /// Verifies that ResolveAsync returns null discovered item failure.
+ ///
+ /// A task representing the asynchronous operation.
+ [Fact]
+ public async Task ResolveAsync_WithNullDiscoveredItem_ReturnsFailure()
+ {
+ // Act
+ var result = await _resolver.ResolveAsync(null!);
+
+ // Assert
+ Assert.False(result.Success);
+ Assert.Contains("cannot be null", result.FirstError);
+ }
+
+ ///
+ /// Verifies that ResolveAsync returns failure when CSV URL is missing.
+ ///
+ /// A task representing the asynchronous operation.
+ [Fact]
+ public async Task ResolveAsync_WithMissingCsvUrl_ReturnsFailure()
+ {
+ // Arrange
+ var discoveredItem = new ContentSearchResult
+ {
+ Id = "test-id",
+ Name = "Test Content",
+ };
+
+ // Act
+ var result = await _resolver.ResolveAsync(discoveredItem);
+
+ // Assert
+ Assert.False(result.Success);
+ Assert.Equal(CsvConstants.CsvUrlNotProvidedError, result.FirstError);
+ }
+
+ ///
+ /// Verifies that ResolveAsync successfully resolves CSV with Generals filtering.
+ ///
+ /// A task representing the asynchronous operation.
+ [Fact]
+ public async Task ResolveAsync_WithGeneralsGame_FiltersCorrectly()
+ {
+ // Arrange
+ const string csvContent = @"relativePath,size,md5,sha256,gameType,language,isRequired,metadata
+Data/INI/GameData.ini,12345,abc123,def456,Generals,All,true,""{""category"":""config""}""
+Data/Lang/English/game.str,67890,f45123,a67890,Generals,EN,true,""{""category"":""language""}""
+Data/INI/ZeroHour.ini,11111,zh111,zh222,ZeroHour,All,true,""{""category"":""config""}""";
+
+ SetupHttpResponse(csvContent);
+
+ var discoveredItem = new ContentSearchResult
+ {
+ Id = "Generals-1.08-All",
+ Name = "Command & Conquer Generals 1.08 (All)",
+ TargetGame = GameType.Generals,
+ };
+ discoveredItem.ResolverMetadata["csvUrl"] = "https://example.com/test.csv";
+ discoveredItem.ResolverMetadata["game"] = "Generals";
+ discoveredItem.ResolverMetadata["version"] = "1.08";
+ discoveredItem.ResolverMetadata["language"] = "All";
+
+ // Act
+ var result = await _resolver.ResolveAsync(discoveredItem);
+
+ // Assert
+ Assert.True(result.Success);
+ Assert.NotNull(result.Data);
+ Assert.Equal(2, result.Data.Files.Count); // Should include Generals files, exclude ZeroHour
+ Assert.Contains(result.Data.Files, f => f.RelativePath == "Data/INI/GameData.ini");
+ Assert.Contains(result.Data.Files, f => f.RelativePath == "Data/Lang/English/game.str");
+ Assert.DoesNotContain(result.Data.Files, f => f.RelativePath == "Data/INI/ZeroHour.ini");
+ }
+
+ ///
+ /// Verifies that ResolveAsync successfully resolves CSV with language filtering.
+ ///
+ /// A task representing the asynchronous operation.
+ [Fact]
+ public async Task ResolveAsync_WithLanguageFilter_FiltersCorrectly()
+ {
+ // Arrange
+ const string csvContent = @"relativePath,size,md5,sha256,gameType,language,isRequired,metadata
+Data/INI/GameData.ini,12345,abc123,def456,Generals,All,true,""{""category"":""config""}""
+Data/Lang/English/game.str,67890,f45123,a67890,Generals,EN,true,""{""category"":""language""}""
+Data/Lang/German/game.str,78901,g45123,g67890,Generals,DE,true,""{""category"":""language""}""";
+
+ SetupHttpResponse(csvContent);
+
+ var discoveredItem = new ContentSearchResult
+ {
+ Id = "Generals-1.08-EN",
+ Name = "Command & Conquer Generals 1.08 (EN)",
+ TargetGame = GameType.Generals,
+ };
+ discoveredItem.ResolverMetadata["csvUrl"] = "https://example.com/test.csv";
+ discoveredItem.ResolverMetadata["game"] = "Generals";
+ discoveredItem.ResolverMetadata["version"] = "1.08";
+ discoveredItem.ResolverMetadata["language"] = "EN";
+
+ // Act
+ var result = await _resolver.ResolveAsync(discoveredItem);
+
+ // Assert
+ Assert.True(result.Success);
+ Assert.NotNull(result.Data);
+ Assert.Equal(2, result.Data.Files.Count); // Should include All and EN files, exclude DE
+ Assert.Contains(result.Data.Files, f => f.RelativePath == "Data/INI/GameData.ini"); // All
+ Assert.Contains(result.Data.Files, f => f.RelativePath == "Data/Lang/English/game.str"); // EN
+ Assert.DoesNotContain(result.Data.Files, f => f.RelativePath == "Data/Lang/German/game.str"); // DE excluded
+ }
+
+ ///
+ /// Verifies that ResolverId returns the correct value.
+ ///
+ [Fact]
+ public void ResolverId_ReturnsCorrectValue()
+ {
+ // Assert
+ Assert.Equal(CsvConstants.CsvResolverId, _resolver.ResolverId);
+ }
+
+ private void SetupHttpResponse(string csvContent)
+ {
+ var response = new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(csvContent),
+ };
+
+ _httpMessageHandlerMock
+ .Protected()
+ .Setup>(
+ "SendAsync",
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
+ .ReturnsAsync(response);
+ }
+}
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameInstallations/LanguageDetectorTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameInstallations/LanguageDetectorTests.cs
new file mode 100644
index 00000000..9a806fff
--- /dev/null
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameInstallations/LanguageDetectorTests.cs
@@ -0,0 +1,160 @@
+using GenHub.Core.Features.GameInstallations;
+using System.IO;
+using Xunit;
+
+namespace GenHub.Tests.Core.Features.GameInstallations;
+
+///
+/// Unit tests for .
+///
+public class LanguageDetectorTests
+{
+ ///
+ /// Verifies that DetectAsync returns "EN" for non-existent directory.
+ ///
+ /// A task representing the asynchronous operation.
+ [Fact]
+ public async Task DetectAsync_NonExistentDirectory_ReturnsEnglish()
+ {
+ // Arrange
+ var detector = new LanguageDetector();
+
+ // Act
+ var result = await detector.DetectAsync("NonExistentPath");
+
+ // Assert
+ Assert.Equal("EN", result);
+ }
+
+ ///
+ /// Verifies that DetectAsync detects English from directory pattern.
+ ///
+ /// A task representing the asynchronous operation.
+ [Fact]
+ public async Task DetectAsync_EnglishDirectory_ReturnsEN()
+ {
+ // Arrange
+ var detector = new LanguageDetector();
+ var tempDir = Path.Combine(Path.GetTempPath(), "TestEnglish");
+ Directory.CreateDirectory(Path.Combine(tempDir, "Data", "english"));
+
+ try
+ {
+ // Act
+ var result = await detector.DetectAsync(tempDir);
+
+ // Assert
+ Assert.Equal("EN", result);
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ ///
+ /// Verifies that DetectAsync detects German from directory pattern.
+ ///
+ /// A task representing the asynchronous operation.
+ [Fact]
+ public async Task DetectAsync_GermanDirectory_ReturnsDE()
+ {
+ // Arrange
+ var detector = new LanguageDetector();
+ var tempDir = Path.Combine(Path.GetTempPath(), "TestGerman");
+ Directory.CreateDirectory(Path.Combine(tempDir, "Data", "german"));
+
+ try
+ {
+ // Act
+ var result = await detector.DetectAsync(tempDir);
+
+ // Assert
+ Assert.Equal("DE", result);
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ ///
+ /// Verifies that DetectAsync detects English from file pattern.
+ ///
+ /// A task representing the asynchronous operation.
+ [Fact]
+ public async Task DetectAsync_EnglishFile_ReturnsEN()
+ {
+ // Arrange
+ var detector = new LanguageDetector();
+ var tempDir = Path.Combine(Path.GetTempPath(), "TestEnglishFile");
+ Directory.CreateDirectory(tempDir);
+ File.WriteAllText(Path.Combine(tempDir, "English.big"), "test");
+
+ try
+ {
+ // Act
+ var result = await detector.DetectAsync(tempDir);
+
+ // Assert
+ Assert.Equal("EN", result);
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ ///
+ /// Verifies that DetectAsync detects Zero Hour English from file pattern.
+ ///
+ /// A task representing the asynchronous operation.
+ [Fact]
+ public async Task DetectAsync_ZeroHourEnglishFile_ReturnsEN()
+ {
+ // Arrange
+ var detector = new LanguageDetector();
+ var tempDir = Path.Combine(Path.GetTempPath(), "TestZHEnglish");
+ Directory.CreateDirectory(tempDir);
+ File.WriteAllText(Path.Combine(tempDir, "EnglishZH.big"), "test");
+
+ try
+ {
+ // Act
+ var result = await detector.DetectAsync(tempDir);
+
+ // Assert
+ Assert.Equal("EN", result);
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ ///
+ /// Verifies that DetectAsync returns "EN" as fallback when no patterns match.
+ ///
+ /// A task representing the asynchronous operation.
+ [Fact]
+ public async Task DetectAsync_NoPatternsMatch_ReturnsEnglishFallback()
+ {
+ // Arrange
+ var detector = new LanguageDetector();
+ var tempDir = Path.Combine(Path.GetTempPath(), "TestEmpty");
+ Directory.CreateDirectory(tempDir);
+
+ try
+ {
+ // Act
+ var result = await detector.DetectAsync(tempDir);
+
+ // Assert
+ Assert.Equal("EN", result);
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+}
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Models/Content/ContentSearchQueryTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Models/Content/ContentSearchQueryTests.cs
new file mode 100644
index 00000000..51f56422
--- /dev/null
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Models/Content/ContentSearchQueryTests.cs
@@ -0,0 +1,93 @@
+using GenHub.Core.Models.Content;
+using Xunit;
+
+namespace GenHub.Tests.Core.Models.Content;
+
+///
+/// Unit tests for .
+///
+public class ContentSearchQueryTests
+{
+ ///
+ /// Verifies that Language property accepts null value.
+ ///
+ [Fact]
+ public void Language_AcceptsNullValue()
+ {
+ // Arrange & Act
+ var query = new ContentSearchQuery { Language = null };
+
+ // Assert
+ Assert.Null(query.Language);
+ }
+
+ ///
+ /// Verifies that Language property accepts lowercase input.
+ ///
+ [Fact]
+ public void Language_AcceptsLowercaseInput()
+ {
+ // Arrange & Act
+ var query = new ContentSearchQuery { Language = "en" };
+
+ // Assert
+ Assert.Equal("EN", query.Language);
+ }
+
+ ///
+ /// Verifies that Language property accepts uppercase input.
+ ///
+ [Fact]
+ public void Language_AcceptsUppercaseInput()
+ {
+ // Arrange & Act
+ var query = new ContentSearchQuery { Language = "EN" };
+
+ // Assert
+ Assert.Equal("EN", query.Language);
+ }
+
+ ///
+ /// Verifies that Language property accepts supported language codes.
+ ///
+ /// The input language code.
+ /// The expected normalized language code.
+ [Theory]
+ [InlineData("All", "All")]
+ [InlineData("EN", "EN")]
+ [InlineData("DE", "DE")]
+ [InlineData("FR", "FR")]
+ [InlineData("ES", "ES")]
+ [InlineData("IT", "IT")]
+ [InlineData("KO", "KO")]
+ [InlineData("PL", "PL")]
+ [InlineData("PT-BR", "PT-BR")]
+ [InlineData("ZH-CN", "ZH-CN")]
+ [InlineData("ZH-TW", "ZH-TW")]
+ [InlineData("pt", "PT-BR")]
+ [InlineData("zh", "ZH-CN")]
+ [InlineData("all", "All")]
+ [InlineData(" en ", "EN")]
+ [InlineData("PT_BR", "PT-BR")]
+ public void Language_AcceptsSupportedLanguageCodes(string input, string expected)
+ {
+ // Arrange & Act
+ var query = new ContentSearchQuery { Language = input };
+
+ // Assert
+ Assert.Equal(expected, query.Language);
+ }
+
+ ///
+ /// Verifies that default Language property is null.
+ ///
+ [Fact]
+ public void Language_DefaultValueIsNull()
+ {
+ // Arrange & Act
+ var query = new ContentSearchQuery();
+
+ // Assert
+ Assert.Null(query.Language);
+ }
+}
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Models/Content/CsvCatalogEntryTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Models/Content/CsvCatalogEntryTests.cs
new file mode 100644
index 00000000..3d6df98f
--- /dev/null
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Models/Content/CsvCatalogEntryTests.cs
@@ -0,0 +1,86 @@
+using GenHub.Core.Models.Content;
+using Xunit;
+
+namespace GenHub.Tests.Core.Models.Content;
+
+///
+/// Unit tests for .
+///
+public class CsvCatalogEntryTests
+{
+ ///
+ /// Verifies that CsvCatalogEntry can be instantiated with default values.
+ ///
+ [Fact]
+ public void Constructor_DefaultValues_AreCorrect()
+ {
+ // Arrange & Act
+ var entry = new CsvCatalogEntry();
+
+ // Assert
+ Assert.Equal(string.Empty, entry.RelativePath);
+ Assert.Equal(0, entry.Size);
+ Assert.Equal(string.Empty, entry.Md5);
+ Assert.Equal(string.Empty, entry.Sha256);
+ Assert.Equal("Generals", entry.GameType);
+ Assert.Equal("All", entry.Language);
+ Assert.True(entry.IsRequired);
+ Assert.Null(entry.Metadata);
+ }
+
+ ///
+ /// Verifies that all properties can be set and retrieved.
+ ///
+ [Fact]
+ public void Properties_CanBeSetAndRetrieved()
+ {
+ // Arrange
+ var entry = new CsvCatalogEntry
+ {
+ RelativePath = "Data/INI/GameData.ini",
+ Size = 12345,
+ Md5 = "abc123",
+ Sha256 = "def456",
+ GameType = "Generals",
+ Language = "EN",
+ IsRequired = true,
+ Metadata = "{\"category\":\"config\"}",
+ };
+
+ // Assert
+ Assert.Equal("Data/INI/GameData.ini", entry.RelativePath);
+ Assert.Equal(12345, entry.Size);
+ Assert.Equal("abc123", entry.Md5);
+ Assert.Equal("def456", entry.Sha256);
+ Assert.Equal("Generals", entry.GameType);
+ Assert.Equal("EN", entry.Language);
+ Assert.True(entry.IsRequired);
+ Assert.Equal("{\"category\":\"config\"}", entry.Metadata);
+ }
+
+ ///
+ /// Verifies that Language defaults to "All".
+ ///
+ [Fact]
+ public void Language_DefaultsToAll()
+ {
+ // Arrange & Act
+ var entry = new CsvCatalogEntry();
+
+ // Assert
+ Assert.Equal("All", entry.Language);
+ }
+
+ ///
+ /// Verifies that IsRequired defaults to true.
+ ///
+ [Fact]
+ public void IsRequired_DefaultsToTrue()
+ {
+ // Arrange & Act
+ var entry = new CsvCatalogEntry();
+
+ // Assert
+ Assert.True(entry.IsRequired);
+ }
+}
diff --git a/GenHub/GenHub.sln b/GenHub/GenHub.sln
index c91c40dd..2d557fb9 100644
--- a/GenHub/GenHub.sln
+++ b/GenHub/GenHub.sln
@@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenHub.Tests.Linux", "GenHu
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenHub.Tests.Windows", "GenHub.Tests\GenHub.Tests.Windows\GenHub.Tests.Windows.csproj", "{904D2AD7-8DBF-4E7C-8FFF-8BFA0EF0E801}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CsvGenerationUtility", "CsvGenerationUtility\CsvGenerationUtility.csproj", "{99348A96-5066-4D9F-9DEC-6D30CB17E389}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -115,6 +117,18 @@ Global
{904D2AD7-8DBF-4E7C-8FFF-8BFA0EF0E801}.Release|x64.Build.0 = Release|Any CPU
{904D2AD7-8DBF-4E7C-8FFF-8BFA0EF0E801}.Release|x86.ActiveCfg = Release|Any CPU
{904D2AD7-8DBF-4E7C-8FFF-8BFA0EF0E801}.Release|x86.Build.0 = Release|Any CPU
+ {99348A96-5066-4D9F-9DEC-6D30CB17E389}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {99348A96-5066-4D9F-9DEC-6D30CB17E389}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {99348A96-5066-4D9F-9DEC-6D30CB17E389}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {99348A96-5066-4D9F-9DEC-6D30CB17E389}.Debug|x64.Build.0 = Debug|Any CPU
+ {99348A96-5066-4D9F-9DEC-6D30CB17E389}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {99348A96-5066-4D9F-9DEC-6D30CB17E389}.Debug|x86.Build.0 = Debug|Any CPU
+ {99348A96-5066-4D9F-9DEC-6D30CB17E389}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {99348A96-5066-4D9F-9DEC-6D30CB17E389}.Release|Any CPU.Build.0 = Release|Any CPU
+ {99348A96-5066-4D9F-9DEC-6D30CB17E389}.Release|x64.ActiveCfg = Release|Any CPU
+ {99348A96-5066-4D9F-9DEC-6D30CB17E389}.Release|x64.Build.0 = Release|Any CPU
+ {99348A96-5066-4D9F-9DEC-6D30CB17E389}.Release|x86.ActiveCfg = Release|Any CPU
+ {99348A96-5066-4D9F-9DEC-6D30CB17E389}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/GenHub/GenHub/Features/Content/Services/ContentDiscoverers/CSVDiscoverer.cs b/GenHub/GenHub/Features/Content/Services/ContentDiscoverers/CSVDiscoverer.cs
new file mode 100644
index 00000000..61ce5655
--- /dev/null
+++ b/GenHub/GenHub/Features/Content/Services/ContentDiscoverers/CSVDiscoverer.cs
@@ -0,0 +1,224 @@
+using GenHub.Core.Constants;
+using GenHub.Core.Interfaces.Common;
+using GenHub.Core.Interfaces.Content;
+using GenHub.Core.Models.Content;
+using GenHub.Core.Models.Enums;
+using GenHub.Core.Models.Manifest;
+using GenHub.Core.Models.Results;
+using GenHub.Features.Manifest;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace GenHub.Features.Content.Services.ContentDiscoverers;
+
+///
+/// CSV content discoverer that discovers content from CSV files.
+/// Implements for CSV-based content discovery.
+///
+///
+/// This class provides discovery capabilities for CSV-based content sources.
+/// It creates objects with metadata pointing to CSV files
+/// that can be resolved by the corresponding CSV resolver.
+/// The discoverer supports different game types and versions, providing appropriate
+/// CSV URLs and metadata for each supported content type.
+///
+public class CSVDiscoverer(
+ ILogger logger,
+ HttpClient httpClient) : IContentDiscoverer
+{
+ ///
+ /// Gets the resolver identifier for CSV content.
+ ///
+ /// A string identifier used to match this discoverer with appropriate resolvers.
+ ///
+ /// This identifier is used by the content resolution system to associate discovered
+ /// items with the appropriate resolver implementation.
+ /// The value "CSVResolver" indicates that discovered items should be resolved using CSV-specific logic.
+ ///
+ public static string ResolverId => CsvConstants.CsvResolverId;
+
+ ///
+ public string SourceName => CsvConstants.CsvSourceName;
+
+ ///
+ public string Description => "Discovers content from CSV files.";
+
+ ///
+ public bool IsEnabled => true;
+
+ ///
+ public ContentSourceCapabilities Capabilities =>
+ ContentSourceCapabilities.SupportsManifestGeneration |
+ ContentSourceCapabilities.DirectSearch |
+ ContentSourceCapabilities.SupportsPackageAcquisition;
+
+ ///
+ /// Discovers content from CSV files based on the search query.
+ ///
+ /// The search criteria containing game type and other filters.
+ /// A token to cancel the operation.
+ /// A containing discovered objects.
+ ///
+ /// This method examines the to determine what type of content to discover.
+ /// Currently supports discovery of:
+ ///
+ /// - version 1.08 content
+ /// - version 1.04 content
+ ///
+ /// For each supported game type, it creates a with appropriate
+ /// metadata including CSV URLs and resolver information. The results can then be resolved
+ /// by the corresponding CSV resolver to obtain full objects.
+ /// Language parameters are normalized to uppercase and included in the manifest ID generation.
+ ///
+ public async Task>> DiscoverAsync(
+ ContentSearchQuery query,
+ CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ // Normalize language to uppercase
+ var normalizedLanguage = string.IsNullOrWhiteSpace(query.Language)
+ ? "All"
+ : query.Language.ToUpperInvariant();
+
+ // Try to load from index.json first
+ var indexUrl = CsvConstants.DefaultCsvIndexUrl; // Use default for now
+ CsvRegistryIndex? registryIndex = null;
+
+ try
+ {
+ logger.LogDebug("Attempting to load CSV registry from index.json: {Url}", indexUrl);
+ var indexJson = await httpClient.GetStringAsync(indexUrl, cancellationToken);
+ registryIndex = JsonSerializer.Deserialize(indexJson, new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true,
+ });
+
+ if (registryIndex?.Registries == null || !registryIndex.Registries.Any())
+ {
+ logger.LogWarning("Index.json loaded but contains no valid registries");
+ registryIndex = null;
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.LogWarning(ex, "Failed to load index.json from {Url}, falling back to configuration", indexUrl);
+ }
+
+ var results = new List();
+
+ // Generals 1.08
+ if (query.TargetGame == GameType.Generals)
+ {
+ var searchResult = CreateSearchResult("Generals", "1.08", normalizedLanguage, registryIndex);
+ if (searchResult != null)
+ {
+ results.Add(searchResult);
+ }
+ }
+
+ // Zero Hour 1.04
+ if (query.TargetGame == GameType.ZeroHour)
+ {
+ var searchResult = CreateSearchResult("ZeroHour", "1.04", normalizedLanguage, registryIndex);
+ if (searchResult != null)
+ {
+ results.Add(searchResult);
+ }
+ }
+
+ return OperationResult>.CreateSuccess(results);
+ }
+ catch (OperationCanceledException)
+ {
+ logger.LogWarning("CSV discovery was cancelled");
+ return OperationResult>.CreateFailure("Operation cancelled");
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "CSV discovery failed");
+ return OperationResult>.CreateFailure($"Discovery failed: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Generates a deterministic manifest ID for CSV content.
+ ///
+ /// The game type (Generals or ZeroHour).
+ /// The game version.
+ /// The language code (normalized to uppercase).
+ /// A deterministic manifest ID in the format "GameType-Version-Language".
+ private static string GenerateManifestId(string gameType, string version, string language)
+ {
+ return $"{gameType}-{version}-{language}";
+ }
+
+ ///
+ /// Creates a ContentSearchResult for the specified game, using registry data if available.
+ ///
+ /// The game type (Generals or ZeroHour).
+ /// The game version.
+ /// The normalized language code.
+ /// The loaded registry index, or null if not available.
+ /// A ContentSearchResult, or null if no suitable registry found.
+ private ContentSearchResult? CreateSearchResult(
+ string gameType,
+ string version,
+ string language,
+ CsvRegistryIndex? registryIndex)
+ {
+ string csvUrl;
+ List? supportedLanguages = null;
+
+ // Try to get data from registry index
+ if (registryIndex != null)
+ {
+ var registry = registryIndex.Registries
+ .FirstOrDefault(r => r.GameType == gameType && r.Version == version && r.IsActive);
+
+ if (registry != null)
+ {
+ csvUrl = registry.Url;
+ supportedLanguages = registry.Languages;
+ }
+ else
+ {
+ logger.LogWarning("No active registry found for {GameType} {Version} in index.json", gameType, version);
+ return null;
+ }
+ }
+ else
+ {
+ // Fallback to default URLs
+ csvUrl = gameType == "Generals" ? CsvConstants.DefaultGeneralsCsvUrl : CsvConstants.DefaultZeroHourCsvUrl;
+ }
+
+ var manifestId = GenerateManifestId(gameType, version, language);
+ var searchResult = new ContentSearchResult
+ {
+ Id = manifestId,
+ Name = gameType == "Generals"
+ ? $"Command & Conquer Generals {version} ({language})"
+ : $"Command & Conquer Generals: Zero Hour {version} ({language})",
+ ResolverId = ResolverId,
+ };
+
+ searchResult.ResolverMetadata.Add("game", gameType);
+ searchResult.ResolverMetadata.Add("version", version);
+ searchResult.ResolverMetadata.Add("language", language);
+ searchResult.ResolverMetadata.Add("csvUrl", csvUrl);
+
+ if (supportedLanguages != null)
+ {
+ searchResult.ResolverMetadata.Add("supportedLanguages", string.Join(",", supportedLanguages));
+ }
+
+ return searchResult;
+ }
+}
diff --git a/GenHub/GenHub/Features/Content/Services/ContentProviders/CSVContentProvider.cs b/GenHub/GenHub/Features/Content/Services/ContentProviders/CSVContentProvider.cs
new file mode 100644
index 00000000..da6b73e0
--- /dev/null
+++ b/GenHub/GenHub/Features/Content/Services/ContentProviders/CSVContentProvider.cs
@@ -0,0 +1,306 @@
+using GenHub.Core.Constants;
+using GenHub.Core.Interfaces.Content;
+using GenHub.Core.Models.Content;
+using GenHub.Core.Models.Enums;
+using GenHub.Core.Models.Manifest;
+using GenHub.Core.Models.Results;
+using GenHub.Features.Content.Services.ContentProviders;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Security.Cryptography;
+using System.Threading;
+using System.Threading.Tasks;
+
+// This class implemented in a very basic way and needs more work
+// It needs to be split into smaller methods
+// Also, the CSV format should be more robustly defined (e.g. using a library like CsvHelper)
+// Error handling and logging should be improved
+// It needs unit tests
+// It is needed to implement caching for the downloaded CSV file
+// And possibly for the validated manifests as well
+// It needs more validation steps IValidator, contentValidation, etc
+
+///
+/// CSV content provider that provides content from authoritative CSV catalogs.
+/// Implements content discovery, resolution, and delivery for CSV-based content.
+///
+///
+/// This provider orchestrates the complete CSV content pipeline by coordinating
+/// , , and
+/// components specifically designed for CSV-based content sources.
+/// It inherits from to leverage common content provider functionality
+/// while providing CSV-specific implementation details.
+///
+public class CsvContentProvider : BaseContentProvider
+{
+ private readonly IContentDiscoverer _discoverer;
+ private readonly IContentResolver _resolver;
+ private readonly IContentDeliverer _deliverer;
+ private readonly ILogger _logger;
+ private readonly string _csvUrl;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The collection of available content discoverers.
+ /// The collection of available content resolvers.
+ /// The collection of available content deliverers.
+ /// The logger for diagnostic information.
+ /// The content validator service.
+ /// The URL of the CSV file to process.
+ ///
+ /// This constructor sets up the CSV content provider by:
+ ///
+ /// - Finding the appropriate CSV by source name
+ /// - Finding the appropriate CSV by resolver ID
+ /// - Finding the appropriate by source name
+ /// - Storing the for use during content preparation
+ ///
+ /// All parameters are required and the constructor will throw exceptions if required
+ /// components cannot be found in the provided collections.
+ ///
+ public CsvContentProvider(
+ IEnumerable discoverers,
+ IEnumerable resolvers,
+ IEnumerable deliverers,
+ ILogger logger,
+ IContentValidator contentValidator,
+ string csvUrl)
+ : base(contentValidator, logger)
+ {
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _csvUrl = csvUrl ?? throw new ArgumentNullException(nameof(csvUrl));
+
+ _discoverer = discoverers.FirstOrDefault(d => string.Equals(d.SourceName, CsvConstants.CsvSourceName, StringComparison.OrdinalIgnoreCase))
+ ?? throw new ArgumentException("CSV discoverer not found", nameof(discoverers));
+
+ _resolver = resolvers.FirstOrDefault(r => string.Equals(r.ResolverId, CsvConstants.CsvResolverId, StringComparison.OrdinalIgnoreCase))
+ ?? throw new ArgumentException("CSV resolver not found", nameof(resolvers));
+
+ _deliverer = deliverers.FirstOrDefault(d => string.Equals(d.SourceName, CsvConstants.FileSystemSourceName, StringComparison.OrdinalIgnoreCase))
+ ?? throw new ArgumentException("FileSystem deliverer not found", nameof(deliverers));
+ }
+
+ ///
+ public override string SourceName => CsvConstants.CsvSourceName;
+
+ ///
+ public override string Description => "Content Provider backed by authoritative CSV catalog";
+
+ ///
+ public override bool IsEnabled => true;
+
+ ///
+ public override ContentSourceCapabilities Capabilities =>
+ ContentSourceCapabilities.RequiresDiscovery |
+ ContentSourceCapabilities.SupportsManifestGeneration |
+ ContentSourceCapabilities.LocalFileDelivery;
+
+ ///
+ protected override IContentDiscoverer Discoverer => _discoverer;
+
+ ///
+ protected override IContentResolver Resolver => _resolver;
+
+ ///
+ protected override IContentDeliverer Deliverer => _deliverer;
+
+ ///
+ /// Gets validated content by content ID from the CSV catalog.
+ ///
+ /// The unique identifier of the content.
+ /// A token to cancel the operation.
+ /// A containing the validated content manifest.
+ ///
+ /// This method performs content lookup by ID through the CSV pipeline:
+ ///
+ /// - Creates a with the content ID as search term
+ /// - Uses the inherited SearchAsync method to find matching content in the CSV catalog
+ /// - Extracts the from the first search result
+ /// - Returns success with the manifest or failure if not found
+ ///
+ /// The method supports cancellation and handles cases where content is not found gracefully.
+ ///
+ public override async Task> GetValidatedContentAsync(
+ string contentId,
+ CancellationToken cancellationToken = default)
+ {
+ var query = new ContentSearchQuery
+ {
+ SearchTerm = contentId,
+ Take = ContentConstants.SingleResultQueryLimit,
+ };
+ var searchResult = await SearchAsync(query, cancellationToken);
+ if (!searchResult.Success || !searchResult.Data!.Any())
+ {
+ return OperationResult.CreateFailure($"{CsvConstants.ContentNotFoundError}: {contentId}");
+ }
+
+ var result = searchResult.Data!.First();
+ var manifest = result.GetData();
+ return manifest != null
+ ? OperationResult.CreateSuccess(manifest)
+ : OperationResult.CreateFailure(CsvConstants.ManifestNotAvailableError);
+ }
+
+ ///
+ /// Prepares content internally by downloading and validating CSV data.
+ ///
+ /// The content manifest to prepare.
+ /// The working directory for file operations.
+ /// Optional progress reporter for the operation.
+ /// A token to cancel the operation.
+ /// A containing the prepared manifest.
+ ///
+ /// This method implements the core CSV content preparation logic:
+ ///
+ /// - Downloads the CSV file from the configured URL using
+ /// - Parses the CSV content line by line, skipping the header row
+ /// - Validates each file entry by checking existence, size, MD5, and SHA256 hashes
+ /// - Creates objects for valid files and adds them to the manifest
+ /// - Reports progress through the interface during download and validation phases
+ /// - Handles validation errors gracefully and returns detailed error messages
+ ///
+ /// The method uses cryptographic hashing (, ) to ensure file integrity
+ /// and supports cancellation throughout the operation.
+ ///
+ protected override async Task> PrepareContentInternalAsync(
+ ContentManifest manifest,
+ string workingDirectory,
+ IProgress? progress,
+ CancellationToken cancellationToken)
+ {
+ var errors = new List();
+
+ try
+ {
+ _logger.LogDebug(CsvConstants.StartingCsvPreparationDebug, manifest.Id);
+ progress?.Report(new ContentAcquisitionProgress
+ {
+ Phase = ContentAcquisitionPhase.Downloading,
+ CurrentOperation = CsvConstants.DownloadingCsvMessage,
+ });
+
+ // CSV download
+ // Need refactoring and splitting into smaller methods
+ string csvContent;
+ using (var httpClient = new HttpClient())
+ {
+ csvContent = await httpClient.GetStringAsync(_csvUrl, cancellationToken);
+ }
+
+ progress?.Report(new ContentAcquisitionProgress
+ {
+ Phase = ContentAcquisitionPhase.ValidatingFiles,
+ CurrentOperation = CsvConstants.ParsingCsvMessage,
+ });
+
+ var files = new List();
+
+ using (var reader = new StringReader(csvContent))
+ {
+ string? line;
+ bool isFirstLine = true;
+ while ((line = await reader.ReadLineAsync()) != null)
+ {
+ if (isFirstLine)
+ {
+ isFirstLine = false; // header
+ continue;
+ }
+
+ if (string.IsNullOrWhiteSpace(line))
+ {
+ continue;
+ }
+
+ var columns = line.Split(CsvConstants.CsvDelimiter);
+ if (columns.Length < CsvConstants.MinCsvColumns)
+ {
+ continue;
+ }
+
+ var relativePath = columns[CsvConstants.RelativePathColumnIndex].Trim();
+ var sizeString = columns[CsvConstants.SizeColumnIndex].Trim();
+ var md5 = columns[CsvConstants.Md5ColumnIndex].Trim();
+ var sha256 = columns[CsvConstants.Sha256ColumnIndex].Trim();
+
+ if (!long.TryParse(sizeString, out var size))
+ {
+ errors.Add(string.Format(CsvConstants.InvalidSizeWarning, relativePath));
+ continue;
+ }
+
+ var absolutePath = Path.Combine(workingDirectory, relativePath);
+ if (!File.Exists(absolutePath))
+ {
+ errors.Add(string.Format(CsvConstants.FileMissingWarning, relativePath));
+ continue;
+ }
+
+ var fileValid = true;
+
+ // Check MD5
+ using (var md5Algorithm = MD5.Create())
+ using (var stream = File.OpenRead(absolutePath))
+ {
+ var computedMd5 = BitConverter.ToString(md5Algorithm.ComputeHash(stream)).Replace("-", string.Empty).ToLowerInvariant();
+ if (!string.Equals(computedMd5, md5, StringComparison.OrdinalIgnoreCase))
+ {
+ errors.Add($"MD5 mismatch: {relativePath}");
+ fileValid = false;
+ }
+ }
+
+ // Check SHA256
+ using (var shaAlgorithm = SHA256.Create())
+ using (var stream = File.OpenRead(absolutePath))
+ {
+ var computedSha = BitConverter.ToString(shaAlgorithm.ComputeHash(stream)).Replace("-", string.Empty).ToLowerInvariant();
+ if (!string.Equals(computedSha, sha256, StringComparison.OrdinalIgnoreCase))
+ {
+ errors.Add($"SHA256 mismatch: {relativePath}");
+ fileValid = false;
+ }
+ }
+
+ if (fileValid)
+ {
+ files.Add(new ManifestFile
+ {
+ RelativePath = relativePath,
+ Size = size,
+ Hash = md5,
+ SourceType = ContentSourceType.LocalFile,
+ });
+ }
+ }
+ }
+
+ manifest.Files.Clear();
+ manifest.Files.AddRange(files);
+
+ // Validation
+ // e.g var validationResult = await IValidate(manifest, cancellationToken);
+ // Need more validation steps here
+ if (errors.Any())
+ {
+ var errorMessage = string.Join("; ", errors);
+ _logger.LogWarning("CSV validation completed with errors: {Errors}", errorMessage);
+ return OperationResult.CreateFailure(errorMessage);
+ }
+
+ _logger.LogDebug("CSV content preparation completed successfully for manifest {ManifestId}", manifest.Id);
+ return OperationResult.CreateSuccess(manifest);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "CSV content preparation failed for manifest {ManifestId}", manifest.Id);
+ return OperationResult.CreateFailure($"CSV content preparation failed: {ex.Message}");
+ }
+ }
+}
diff --git a/GenHub/GenHub/Features/Content/Services/ContentResolvers/CSVResolver.cs b/GenHub/GenHub/Features/Content/Services/ContentResolvers/CSVResolver.cs
new file mode 100644
index 00000000..23d04b7e
--- /dev/null
+++ b/GenHub/GenHub/Features/Content/Services/ContentResolvers/CSVResolver.cs
@@ -0,0 +1,317 @@
+using GenHub.Core.Constants;
+using GenHub.Core.Interfaces.Content;
+using GenHub.Core.Models.Content;
+using GenHub.Core.Models.Enums;
+using GenHub.Core.Models.Manifest;
+using GenHub.Core.Models.Results;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace GenHub.Features.Content.Services.ContentResolvers;
+
+///
+/// CSV content resolver that downloads and parses CSV files to create content manifests.
+/// Implements for CSV-based content discovery and resolution.
+///
+///
+/// This class is responsible for resolving discovered CSV content items into full objects.
+/// It downloads CSV files from URLs specified in the discovery metadata, parses the content, and creates
+/// entries for each valid file entry in the CSV.
+/// The resolver supports filtering by game type and language to ensure only relevant files are included.
+///
+public class CSVResolver(HttpClient httpClient, ILogger logger) : IContentResolver
+{
+ private readonly HttpClient _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
+ private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+
+ ///
+ /// Gets the resolver identifier for CSV content.
+ ///
+ /// A string identifier used to match this resolver with discovered content.
+ ///
+ /// This identifier is used by the content discovery system to associate discovered
+ /// items with this resolver implementation.
+ /// The value "CSVResolver" indicates that this resolver handles CSV-based content.
+ ///
+ public string ResolverId => CsvConstants.CsvResolverId;
+
+ ///
+ /// Resolve a discovered CSV item into a by downloading and parsing the CSV,
+ /// filtering rows by game and language, and producing entries.
+ ///
+ /// The discovered content item containing CSV metadata.
+ /// A token to cancel the operation.
+ /// An containing the resolved .
+ ///
+ /// This method performs the following steps:
+ ///
+ /// - Extracts the CSV URL from the using multiple fallback strategies
+ /// - Determines the target game type and language from discovery metadata
+ /// - Downloads the CSV file from the extracted URL
+ /// - Parses the CSV content line by line, validating each entry
+ /// - Filters entries based on game type, version, and language criteria
+ /// - Creates objects for valid entries
+ /// - Constructs and returns a complete
+ ///
+ /// The method supports cancellation through the and handles
+ /// various error conditions gracefully by returning appropriate failure results.
+ ///
+ public async Task> ResolveAsync(
+ ContentSearchResult discoveredItem,
+ CancellationToken cancellationToken = default)
+ {
+ if (discoveredItem == null)
+ {
+ return OperationResult.CreateFailure("discoveredItem cannot be null");
+ }
+
+ try
+ {
+ // Obtain CSV URL from common locations
+ string? csvUrl = null;
+
+ // Try ResolverMetadata (common pattern in other discoverers)
+ try
+ {
+ // Some ContentSearchResult implementations expose a dictionary property named ResolverMetadata
+ // We'll attempt to read it via dynamic/Reflection style if typed property isn't available.
+ var resolverMetadata = discoveredItem.GetType().GetProperty("ResolverMetadata")?.GetValue(discoveredItem);
+ if (resolverMetadata is IDictionary dict && dict.TryGetValue("csvUrl", out var url))
+ {
+ csvUrl = url;
+ }
+ }
+ catch
+ {
+ // Ignore reflection failures; we'll try other locations below
+ }
+
+ // Fallback to SourceUrl
+ if (string.IsNullOrWhiteSpace(csvUrl) && !string.IsNullOrWhiteSpace(discoveredItem.SourceUrl))
+ {
+ csvUrl = discoveredItem.SourceUrl;
+ }
+
+ // Fallback to generic Metadata dictionary if present
+ if (string.IsNullOrWhiteSpace(csvUrl))
+ {
+ try
+ {
+ var metadataProperty = discoveredItem.GetType().GetProperty("Metadata");
+ if (metadataProperty != null)
+ {
+ var metadataValue = metadataProperty.GetValue(discoveredItem);
+ if (metadataValue is IDictionary metadataDict && metadataDict.TryGetValue("csvUrl", out var url))
+ {
+ csvUrl = url;
+ }
+ }
+ }
+ catch
+ {
+ // Ignore
+ }
+ }
+
+ if (string.IsNullOrWhiteSpace(csvUrl))
+ {
+ _logger.LogError(CsvConstants.CsvUrlMissingLog, discoveredItem?.Id);
+ return OperationResult.CreateFailure(CsvConstants.CsvUrlNotProvidedError);
+ }
+
+ // Determine requested target game (if the discoverer set it)
+ var targetGame = discoveredItem.TargetGame != default ? discoveredItem.TargetGame : (GameType?)null;
+
+ // Also allow resolver metadata 'game' fallback
+ try
+ {
+ var resolverMetadata = discoveredItem.GetType().GetProperty("ResolverMetadata")?.GetValue(discoveredItem);
+ if ((targetGame == null) && resolverMetadata is IDictionary dict && dict.TryGetValue("game", out var gameString))
+ {
+ if (Enum.TryParse(gameString, true, out var parsedGame))
+ {
+ targetGame = parsedGame;
+ }
+ }
+ }
+ catch
+ {
+ // Ignore
+ }
+
+ // Determine requested language (if the discoverer set it)
+ string? targetLanguage = null;
+ try
+ {
+ var resolverMetadata = discoveredItem.GetType().GetProperty("ResolverMetadata")?.GetValue(discoveredItem);
+ if (resolverMetadata is IDictionary dict && dict.TryGetValue("language", out var languageString))
+ {
+ targetLanguage = languageString;
+ }
+ }
+ catch
+ {
+ // Ignore
+ }
+
+ // Download CSV as stream
+ using var response = await _httpClient.GetAsync(csvUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
+ if (!response.IsSuccessStatusCode)
+ {
+ _logger.LogError(CsvConstants.CsvDownloadFailedLog, csvUrl, response.StatusCode);
+ return OperationResult.CreateFailure($"{CsvConstants.CsvDownloadFailedError}: {response.StatusCode}");
+ }
+
+ using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
+ using var reader = new StreamReader(stream);
+
+ // Parse CSV streaming
+ var files = new List();
+ bool isFirstLine = true;
+ long lineIndex = 0;
+
+ while (!reader.EndOfStream)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ var line = await reader.ReadLineAsync();
+ lineIndex++;
+
+ if (line == null)
+ {
+ break;
+ }
+
+ if (isFirstLine)
+ {
+ isFirstLine = false; // Skip header line
+ continue;
+ }
+
+ if (string.IsNullOrWhiteSpace(line))
+ {
+ continue;
+ }
+
+ // Split into up to 6 parts: relPath,size,md5,sha256,version,language
+ var parts = line.Split(new[] { CsvConstants.CsvDelimiter }, CsvConstants.MaxCsvColumns, StringSplitOptions.None);
+ if (parts.Length < CsvConstants.MinCsvColumns)
+ {
+ _logger.LogWarning(CsvConstants.MalformedCsvLineWarning, lineIndex);
+ continue;
+ }
+
+ var relativePath = parts[CsvConstants.RelativePathColumnIndex].Trim();
+ var sizeString = parts[CsvConstants.SizeColumnIndex].Trim();
+ var md5 = parts[CsvConstants.Md5ColumnIndex].Trim();
+ var sha256 = parts[CsvConstants.Sha256ColumnIndex].Trim();
+ string? entryGameType = parts.Length > CsvConstants.GameTypeColumnIndex ? parts[CsvConstants.GameTypeColumnIndex].Trim() : null;
+ string? entryLanguage = parts.Length > CsvConstants.LanguageColumnIndex ? parts[CsvConstants.LanguageColumnIndex].Trim() : null;
+ bool isRequired = parts.Length > CsvConstants.IsRequiredColumnIndex && bool.TryParse(parts[CsvConstants.IsRequiredColumnIndex].Trim(), out var req) ? req : true;
+ string? metadata = parts.Length > CsvConstants.MetadataColumnIndex ? parts[CsvConstants.MetadataColumnIndex].Trim() : null;
+
+ if (string.IsNullOrEmpty(relativePath))
+ {
+ _logger.LogWarning(CsvConstants.EmptyRelativePathWarning, lineIndex);
+ continue;
+ }
+
+ if (!long.TryParse(sizeString, out var size))
+ {
+ _logger.LogWarning(CsvConstants.SizeParseFailedWarning, relativePath, lineIndex);
+ continue;
+ }
+
+ // Decide whether this entry belongs to the requested game and language
+ bool includeEntry = true;
+
+ // Filter by game if specified
+ if (targetGame != null)
+ {
+ includeEntry = false;
+
+ // Prefer explicit gameType column if present
+ if (!string.IsNullOrWhiteSpace(entryGameType))
+ {
+ var gameTypeLower = entryGameType.ToLowerInvariant();
+ if (targetGame == GameType.Generals && gameTypeLower.Contains("generals"))
+ {
+ includeEntry = true;
+ }
+ else if (targetGame == GameType.ZeroHour && gameTypeLower.Contains("zerohour"))
+ {
+ includeEntry = true;
+ }
+ }
+ else
+ {
+ // Fallback: infer from relative path keywords
+ var pathLower = relativePath.ToLowerInvariant();
+ if (targetGame == GameType.Generals && (pathLower.Contains("generals") || pathLower.Contains("generals_") || pathLower.Contains("generals1.08")))
+ {
+ includeEntry = true;
+ }
+
+ if (targetGame == GameType.ZeroHour && (pathLower.Contains("zerohour") || pathLower.Contains("zero") || pathLower.Contains("zh_") || pathLower.Contains("zero_hour")))
+ {
+ includeEntry = true;
+ }
+ }
+ }
+
+ // Filter by language if specified
+ if (includeEntry && !string.IsNullOrWhiteSpace(targetLanguage) && !string.Equals(targetLanguage, "All", StringComparison.OrdinalIgnoreCase))
+ {
+ // Include if language matches or entry is "All" (shared files)
+ includeEntry = string.Equals(entryLanguage, targetLanguage, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(entryLanguage, "All", StringComparison.OrdinalIgnoreCase);
+ }
+
+ // If no rule matched, skip this row
+ if (!includeEntry)
+ {
+ continue;
+ }
+
+ // Create ManifestFile (store sha256 as Hash by default)
+ var manifestFile = new ManifestFile
+ {
+ RelativePath = relativePath,
+ Size = size,
+ Hash = sha256 ?? string.Empty,
+ SourceType = ContentSourceType.Unknown,
+ IsRequired = isRequired,
+ };
+
+ files.Add(manifestFile);
+ }
+
+ // Build ContentManifest
+ var manifest = new ContentManifest
+ {
+ // Name/Version/Publisher etc can be set from discoveredItem if available
+ Name = discoveredItem.Name ?? "CSV Generated Manifest",
+ Version = discoveredItem.Version ?? string.Empty,
+ ContentType = discoveredItem.ContentType != default ? discoveredItem.ContentType : default,
+ TargetGame = discoveredItem.TargetGame != default ? discoveredItem.TargetGame : (targetGame ?? default),
+ Files = files,
+ };
+
+ return OperationResult.CreateSuccess(manifest);
+ }
+ catch (OperationCanceledException)
+ {
+ _logger.LogWarning(CsvConstants.OperationCancelledLog);
+ return OperationResult.CreateFailure(CsvConstants.OperationCancelledError);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, CsvConstants.CsvResolveFailedLog, discoveredItem?.Id);
+ return OperationResult.CreateFailure($"{CsvConstants.CsvResolveFailedError}: {ex.Message}");
+ }
+ }
+}
diff --git a/GenHub/GenHub/Features/Manifest/ManifestProvider.cs b/GenHub/GenHub/Features/Manifest/ManifestProvider.cs
index 5db6a308..93a2188e 100644
--- a/GenHub/GenHub/Features/Manifest/ManifestProvider.cs
+++ b/GenHub/GenHub/Features/Manifest/ManifestProvider.cs
@@ -5,6 +5,7 @@
using System.Threading;
using System.Threading.Tasks;
using GenHub.Core.Interfaces.Manifest;
+using GenHub.Core.Models.Content;
using GenHub.Core.Models.Enums;
using GenHub.Core.Models.GameInstallations;
using GenHub.Core.Models.GameVersions;
@@ -326,7 +327,7 @@ public Task> AddManifestAsync(ContentManifest manifest, st
return Task.FromResult(OperationResult>.CreateSuccess(all));
}
- public Task>> SearchManifestsAsync(Core.Models.Content.ContentSearchQuery query, CancellationToken cancellationToken = default)
+ public Task>> SearchManifestsAsync(ContentSearchQuery query, CancellationToken cancellationToken = default)
{
return Task.FromResult(OperationResult>.CreateSuccess(Array.Empty()));
}
diff --git a/GenHub/GenHub/Features/Validation/GameInstallationValidator.cs b/GenHub/GenHub/Features/Validation/GameInstallationValidator.cs
index 674649cb..b565c315 100644
--- a/GenHub/GenHub/Features/Validation/GameInstallationValidator.cs
+++ b/GenHub/GenHub/Features/Validation/GameInstallationValidator.cs
@@ -2,12 +2,15 @@
using GenHub.Core.Interfaces.Content;
using GenHub.Core.Interfaces.Manifest;
using GenHub.Core.Interfaces.Validation;
+using GenHub.Core.Models.Content;
+using GenHub.Core.Models.Enums;
using GenHub.Core.Models.GameInstallations;
using GenHub.Core.Models.Results;
using GenHub.Core.Models.Validation;
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@@ -22,7 +25,10 @@ public class GameInstallationValidator(
ILogger logger,
IManifestProvider manifestProvider,
IContentValidator contentValidator,
- IFileHashProvider hashProvider)
+ IFileHashProvider hashProvider,
+ IContentDiscoverer? csvDiscoverer = null,
+ IContentResolver? csvResolver = null,
+ HttpClient? httpClient = null)
: FileSystemValidator(logger ?? throw new ArgumentNullException(nameof(logger)), hashProvider ?? throw new ArgumentNullException(nameof(hashProvider))),
IGameInstallationValidator, IValidator
{
@@ -30,6 +36,112 @@ public class GameInstallationValidator(
private readonly IManifestProvider _manifestProvider = manifestProvider ?? throw new ArgumentNullException(nameof(manifestProvider));
private readonly IContentValidator _contentValidator = contentValidator ?? throw new ArgumentNullException(nameof(contentValidator));
private readonly IFileHashProvider _hashProvider = hashProvider ?? throw new ArgumentNullException(nameof(hashProvider));
+ private readonly IContentDiscoverer? _csvDiscoverer = csvDiscoverer;
+ private readonly IContentResolver? _csvResolver = csvResolver;
+ private readonly HttpClient? _httpClient = httpClient;
+
+ ///
+ /// Attempts to validate the installation using CSV-based content discovery and resolution.
+ /// This serves as a fallback or alternative validation method when traditional manifest-based
+ /// validation is not available or desired.
+ ///
+ /// The game installation to validate.
+ /// Progress reporter for MVVM integration.
+ /// A cancellation token.
+ /// A representing the CSV-based validation outcome, or null if CSV validation is not available.
+ private async Task TryCsvValidationAsync(
+ GameInstallation installation,
+ IProgress? progress,
+ CancellationToken cancellationToken)
+ {
+ // Check if CSV components are available
+ if (_csvDiscoverer == null || _csvResolver == null || _httpClient == null)
+ {
+ _logger.LogDebug("CSV validation components not available, skipping CSV validation");
+ return null;
+ }
+
+ try
+ {
+ _logger.LogInformation("Attempting CSV-based validation for installation '{Path}'", installation.InstallationPath);
+
+ // Determine target game for CSV discovery
+ var targetGame = installation.HasGenerals ? GameType.Generals :
+ installation.HasZeroHour ? GameType.ZeroHour : (GameType?)null;
+
+ if (targetGame == null)
+ {
+ _logger.LogDebug("No supported game type found for CSV validation");
+ return null;
+ }
+
+ // Create search query for CSV discovery
+ var query = new ContentSearchQuery
+ {
+ TargetGame = targetGame.Value,
+ Language = "All", // Use "All" to get all language variants
+ ContentType = ContentType.GameInstallation,
+ Take = 1, // We only need the latest version
+ };
+
+ // Discover CSV content
+ progress?.Report(new ValidationProgress(1, 4, "Discovering CSV content"));
+ var discoveryResult = await _csvDiscoverer.DiscoverAsync(query, cancellationToken);
+ if (!discoveryResult.Success || discoveryResult.Data == null || !discoveryResult.Data.Any())
+ {
+ _logger.LogDebug("No CSV content discovered for game type {GameType}", targetGame);
+ return null;
+ }
+
+ var discoveredItem = discoveryResult.Data.First();
+
+ // Resolve CSV content to manifest
+ progress?.Report(new ValidationProgress(2, 4, "Resolving CSV manifest"));
+ var resolutionResult = await _csvResolver.ResolveAsync(discoveredItem, cancellationToken);
+ if (!resolutionResult.Success || resolutionResult.Data == null)
+ {
+ _logger.LogWarning("Failed to resolve CSV content: {Error}", resolutionResult.FirstError);
+ return null;
+ }
+
+ var csvManifest = resolutionResult.Data;
+
+ // Validate the CSV manifest
+ progress?.Report(new ValidationProgress(3, 4, "Validating CSV manifest"));
+ var manifestValidationResult = await _contentValidator.ValidateManifestAsync(csvManifest, cancellationToken);
+ if (!manifestValidationResult.IsValid)
+ {
+ var errors = manifestValidationResult.Issues.Where(i => i.Severity == ValidationSeverity.Error).ToList();
+ if (errors.Any())
+ {
+ _logger.LogWarning("CSV manifest validation failed with {Count} errors", errors.Count);
+ return null;
+ }
+ }
+
+ // Perform full content validation using CSV manifest
+ progress?.Report(new ValidationProgress(4, 4, "Validating content against CSV manifest"));
+ var fullValidation = await _contentValidator.ValidateAllAsync(
+ installation.InstallationPath,
+ csvManifest,
+ progress,
+ cancellationToken);
+
+ _logger.LogInformation(
+ "CSV-based validation completed for '{Path}' with {Count} issues",
+ installation.InstallationPath,
+ fullValidation.Issues.Count);
+
+ return new ValidationResult(
+ installation.InstallationPath,
+ fullValidation.Issues.ToList());
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "CSV validation failed for installation '{Path}'", installation.InstallationPath);
+ return null;
+ }
+ }
///
/// Validates the specified game installation.
@@ -69,6 +181,14 @@ public async Task ValidateAsync(GameInstallation installation,
cancellationToken.ThrowIfCancellationRequested();
if (manifest == null)
{
+ // Try CSV validation as fallback
+ var csvResult = await TryCsvValidationAsync(installation, progress, cancellationToken);
+ if (csvResult != null)
+ {
+ _logger.LogInformation("Using CSV-based validation as manifest fallback succeeded");
+ return csvResult;
+ }
+
issues.Add(new ValidationIssue { IssueType = ValidationIssueType.MissingFile, Path = installation.InstallationPath, Message = "Manifest not found for installation." });
progress?.Report(new ValidationProgress(totalSteps, totalSteps, "Validation complete"));
return new ValidationResult(installation.InstallationPath, issues);
diff --git a/GenHub/GenHub/Infrastructure/DependencyInjection/ContentDeliveryModule.cs b/GenHub/GenHub/Infrastructure/DependencyInjection/ContentDeliveryModule.cs
index 07cff47a..180a4bc0 100644
--- a/GenHub/GenHub/Infrastructure/DependencyInjection/ContentDeliveryModule.cs
+++ b/GenHub/GenHub/Infrastructure/DependencyInjection/ContentDeliveryModule.cs
@@ -43,17 +43,20 @@ public static IServiceCollection AddContentPipelineServices(this IServiceCollect
services.AddTransient();
services.AddTransient();
services.AddTransient();
+ services.AddTransient();
// Register content discoverers
services.AddTransient();
services.AddTransient();
services.AddTransient();
services.AddTransient();
+ services.AddTransient();
// Register content resolvers
services.AddTransient();
services.AddTransient();
services.AddTransient();
+ services.AddTransient();
// Register content deliverers
services.AddTransient();
diff --git a/GenHub/GenHub/Infrastructure/DependencyInjection/ValidationModule.cs b/GenHub/GenHub/Infrastructure/DependencyInjection/ValidationModule.cs
index 7f216a16..0aaa1c33 100644
--- a/GenHub/GenHub/Infrastructure/DependencyInjection/ValidationModule.cs
+++ b/GenHub/GenHub/Infrastructure/DependencyInjection/ValidationModule.cs
@@ -4,9 +4,12 @@
using GenHub.Core.Models.GameVersions;
using GenHub.Core.Models.Manifest;
using GenHub.Features.Content.Services;
+using GenHub.Features.Content.Services.ContentDiscoverers;
+using GenHub.Features.Content.Services.ContentResolvers;
using GenHub.Features.Validation;
using Microsoft.Extensions.DependencyInjection;
using System;
+using System.Net.Http;
namespace GenHub.Infrastructure.DependencyInjection;
@@ -25,6 +28,11 @@ public static IServiceCollection AddValidationServices(this IServiceCollection s
// Register core content validator first (used by all other validators)
services.AddTransient();
+ // Register CSV components for enhanced validation capabilities
+ services.AddTransient();
+ services.AddTransient();
+ services.AddHttpClient();
+
// Register domain-specific validators that use ContentValidator internally
services.AddTransient();
services.AddTransient();
diff --git a/GenHub/GenHub/appsettings.json b/GenHub/GenHub/appsettings.json
index adf1b368..6e56e0ee 100644
--- a/GenHub/GenHub/appsettings.json
+++ b/GenHub/GenHub/appsettings.json
@@ -25,6 +25,12 @@
"DefaultTheme": "Dark",
"DefaultWindowWidth": 1024,
"DefaultWindowHeight": 768
+ },
+ "CSV": {
+ "IndexUrl": "https://raw.githubusercontent.com/Community-Outpost/GenHub/main/docs/GameInstallationFilesRegistry/index.json",
+ "AutoDetectLanguage": true,
+ "CacheTtlMinutes": 60,
+ "StrictMode": false
}
},
"Logging": {
diff --git a/docs/GameInstallationFilesRegistry/Generals-1.08.csv b/docs/GameInstallationFilesRegistry/Generals-1.08.csv
new file mode 100644
index 00000000..799a94b8
--- /dev/null
+++ b/docs/GameInstallationFilesRegistry/Generals-1.08.csv
@@ -0,0 +1,199 @@
+relativePath,size,md5,sha256,gameType,language,isRequired,metadata
+!HotkeysLeikeze.big,284426,7c0a710f49ca1c3a25daf7761fd187b5,368db0e6cac56d0694e615fd1fe2492653f012386e512665114ecdbc8ce28ae5,Generals,EN,False,"{""category"":""other""}"
+00000000.016,153716,aebed2f8fa6f42b8c76929dfc8f90a00,ef61474057b21db70ae4356c3f22c088e583b3fd00c0b21da0c298949e8c3d62,Generals,EN,False,"{""category"":""other""}"
+00000000.256,308276,e8caed1e8eb521287cad86cbcb6edf5c,2d1f66e232557e775e636f8b8976422e330223e70a50f0535cc6b2e5ea03903f,Generals,EN,False,"{""category"":""other""}"
+311_ExpandedLANLobbyMenu.big,104060,00e46ab982c583cc3305666be3cfb1cb,0486bb43d68c93e6d68ad31641f71798aecbf563d7503b19135c9461a48296de,Generals,EN,False,"{""category"":""other""}"
+341_ControlBarPro.big,15602515,ca809a18a61d60462aa3921f862433d9,411225a1967860455ee1bd5ec456c39aa44bbbc837286da561905ca51deba66f,Generals,EN,False,"{""category"":""other""}"
+Audio.big,127940044,c4d80fc87dc13eacd9dbf2a981b194a3,d522df264149e3e27fd46f027548cc38bd60614a43666555219b7b1d45fb3bad,Generals,EN,False,"{""category"":""other""}"
+AudioEnglish.big,104744592,fc015ddbe16ac6b4d39a85f5612d7233,39d67dba96111178fcceefae2bedb2dc65b55b968741162c714b333c0b0f5f2e,Generals,EN,False,"{""category"":""other""}"
+BINKW32.DLL,362760,837f5131bc799a0655ab8197e6035fc5,65a5b16c9ef9ac30ece4b20787dbd923483da942577c54684bc85ecc6214dfbc,Generals,EN,False,"{""category"":""other""}"
+BrowserEngine.dll.bak,360144,8de50d85465a9241d07da13e7c3f92cf,df4b2672c4a148a2ca2af67b2cafccf14cad2f0e0466a2306f1bb8f8848395ad,Generals,EN,False,"{""category"":""other""}"
+Core/Activation.dll,2574896,24b99dcfcfa8cfc86e54cd978a5c44f8,61a5c959e1b6a2c284218aa4494d616d54b83c982de4a5a10047f07f44c5780f,Generals,EN,False,"{""category"":""other""}"
+Core/Activation64.dll,3031088,038fd144b4ac5addce4ec2044beae754,a073118dd286ffa10e527e2a37e41d2482c74a12a537188736bee0a72e85ee30,Generals,EN,False,"{""category"":""other""}"
+d3d8.cfg,23,0942cd1dda84a096cbc59f58b98d3def,05a1409c8de13b698beffb7a653512f6f7adc34a234a145d1395f65bd334b258,Generals,EN,False,"{""category"":""other""}"
+d3d8.dll,3843584,8a88058ccb6d2512df70b4c1c80e1317,be5276180d04b3de9abd20aeaf2c1f65a2b65c800233ce49d5e77f1ab42441f7,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCAttack_S.ani,1666,548a3180cc4fadcdf669ea455e6e1921,8e1a57bd031bc8565775f78a162eb0b2f81444d464466e57d43838a116b38ab2,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/sccattack.ani,6398,6d899deaefe2228081f28073a9485ade,fa13992a0603390b0c8fa4200cfca7ee9eb0744cb55b87f49aebee55e5711216,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCAttMov_S.ani,1670,3bd9ee4b858660bbf45fa393dc1c04c5,b9c38e1c1a9294b3afcd029162a79bbbfc285770b5e1ff82998a604755ad7e19,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCAttMov.ani,5644,f06b765c53fda761aebe4f8b4045cd08,c6b2ac92c9d8d8a15bd411dc3e3cf6e276b081cef20035ca82647f0168085714,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCCashHack.ani,7896,19efb6c96fb3b30adfcf6d8ad6fa7981,b41565b664457a00d87efbd24b5681155967620df5ae64a54d28aa9780279385,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCEnter_S.ani,2446,f48af1584768ade129a8a6c4f049b452,858479b74fafd998f1496b69edf2e44245a0c2f8bc8dc6c7ecb9b90d62c7370f,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCEnter.ani,2440,20f9bc7c814947b4cec1349c4bb8bfb7,037ab0691bebf9b4a985d194d0619e9fefa0330a07d4c85bb5047c26d10c5787,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCExit.ani,2436,acf7171dfbe1f72d64c9c08a6a190469,590d419191ec781d97fa7e375498eb99270eeccf8d9ee75bb5c3c67e9dcb620a,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCFriendly_S.ani,6358,5578eda24455b8416ac223d29b4ce475,488b83907f2b12c35d1e3d688a3564acd309d4f2afa1185cade157aee40fc238,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCFriendly.ani,6344,9f7a4f16f5cafbf36e865285ec360638,f7b424afb5575067fffe02cf1afdeb53f62f81ee85a12d581e2a76b5ab418652,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCGuard.ani,1616,1b24328b2926ca7bede732be68b4a4c1,fa8c51b348106c3ede587fc0a13deb63aa54a5db38ce8f582f03b761d57f6150,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCHeal.ani,2478,d12a7573375158375a33131339598d51,32997e09311a684687ab8a339d63d3f075eab98def7e8e0c47b4519b9763a695,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCHostile_S.ani,1674,aa715bed9e28d9dd5a13a56b8fec2e90,73f2fb4c8871afbaa49d6a5cbd34c53e4f29cead68f571d7aab5a5b406c893ea,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCHostile.ani,3270,bf03d453be48a42b218c084cbe19e578,78133602f6b1eceb6257895e9eaeadeb07ddbc9e969f6ac69b6bcbe16b5244ed,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCHostile2.ani,8634,f46b0d70e7c29ddb6b24a14dc053fb95,69a82c2f23d5608f039ce47cde2fe955ec1e4dc9971082ef2c24035f04d0738c,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCHostile3.ani,25792,92918392b8969da0d4562b5636df397c,b86c5be45dbf421172de0d12f4f1b237ce1844ad67ce614620490d60a5cbc5b5,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCKnifeAttack.ani,3966,eb68b3991a575c6ee2247c66b9ff01da,922ba292078be4b9f697450bcf68e23b6bcd9344f433b64641c856f15cc040a1,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCMove_S.ani,1664,3b33efc0b64e1b35f07afb8e098d5cf8,0a60c8f9b6da230767f10275ca97a57347d65a39468f953db122e0f93b07793c,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/sccmove.ani,5638,728cd67cd5a9c8c133f4a05d0a7424ea,edcc9a98a24e0e21bd89e45db2fcdf04c5d3ac359616db96ea6c33407df04c82,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCNoAction_S.ani,892,d9612607ce26ba18da3c98187ba8f60a,2cfa77a51eb15ab54359c13c27b926d7b0f948f2c91d4a1928d139d70ceacb02,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCNoAction.ani,886,cc4e179474a670631835d81725830882,116f51bd9073187b7b2fba0ce08bbe3417cd5daacec7b2af6b78a60afd07d6ff,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCNoBomb.ani,886,48237559686d7aaf721d2cb5109efa67,03e4cd887ab8283a8594b4ae54c0f73b159dce2ebf2df069923531de89be0d7a,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCNoEntry_S.ani,1682,121ffb04e7785ef03dba6de7930d5899,2b76be67e3d75d0cc428efb1563c08c0a6f717d95ffdeb1696c70ba446c92c09,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCNoEntry.ani,1676,bf2c1b618dff02c56bed38e7c69b19ba,ab8fed4bf8bf5bf6ff8e79a40bc89afe9c6bc7c13e2b1f5577506bb48d017803,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCNoKnife.ani,886,973eebf5e3893f3e1ce04ebab5f2b5eb,49d8a05655d9af4bf5c6ab3c7f15e2c86dc1037cc2c016c093d22eada35d75f0,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCOutrange.ani,16460,f18891ca6f6be7f2bc8feeeda196ac80,8539c4a441775dd7ccf5c1faf0cb79fdd5fb6c6797f7d7d3ca98768348429741,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCPlace.ani,1680,dc039f3410c790f3666ac7dc611ef801,a149ac8f358c82b6fd89e2e3fef32b6b1d11adcfd846c3560fd388d75c166e0b,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCPlaceBeacon.ani,7078,82c144ddda013cf670954944a79b2196,d32b4336cbe21101cb872fe9e650f0cad9c77f86fdc26286484448c3fc00b8a6,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/sccpointer.ani,884,54199240e4efefa204418c6c973221b2,ad3829b3e6262f8881ad2a3ddc90e7add848ca5b6bb8fda76facd6ea3a98df00,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCRallyPnt_S.ani,1668,1b3d007599e9307b758c28748321c10b,cfb1cd7baed30b94d5b47a659296738376cafeb4dc492dda6b2c9e5de534d1b9,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCRallyPnt.ani,4064,cec0a2074598b34bebe51d14f7783d31,9d15def280ae76cc3aff9f5ae61547f035b9841ed42e647d13d4cae596a2065d,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCRemoteChg.ani,9412,f8a2450fc0f82f28f584af001dbfbc30,d60b49ac06ae56b56fc03e6cbbf6d1bdbf0bc66eaacec621d7cd0caffd300d6d,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCRepair.ani,5640,a7a77357ec53203c1af9164acf6abff7,baaaa22fb2b331660321833659967d8d057cb50ab31cd2b927459ff0c548ecb6,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCResumeC.ani,17970,a5941a513b20dc2165e7ed9fd2cfdf12,cea73ea5cf0dc34d8299d42ec965eaf2c98a7a501ab1bd5599407f3af91f7e85,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/sccscroll0.ani,888,69a1fbc2773ec932f6a5a7fb37f7f12f,0560079fe4782ac89942eb45e94de7011eb1f4ebb39230890e4917d612bc5d01,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/sccscroll1.ani,888,6aacbae0874a6c9aa335e35947a8ebd0,c48c8691866637e0fa9fc98bb40e2d8240370b4cafd7b767c1595b58789eb06c,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/sccscroll2.ani,888,43f6019d566d0aa8759a42d0a5bec5cd,e62fe7da074898004ee7ef81626830fa94332b3a9d4fe8987e948cea0e2ac1f9,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/sccscroll3.ani,888,6f2e50e14ff2776eae76d961a4a5bfc7,f9c2a000c946a6f6bb420d9671c9ba21a9fc14821a5692a5f2e5b4755fdebb8d,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCScroll4.ani,888,a169748d79da826c10d7e5149c7fd41e,ec77136c9768ca3c50216ae92269aa5fae4f29197a13a355c6f92fcf53ecb279,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCScroll5.ani,888,f17a6fc61a7b944bf5eed66d8242d5ca,2ccb9140e23faf6cc98560e545a54b83efce54dd1bfb01ea84119732d0f04433,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCScroll6.ani,888,33618b4ed32fcec63df6fea44b98bac8,46186f3e8e9243edda6e0baf00e0a0fcaaf5e5d9b4d4531bf13393fdda67acc9,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCScroll7.ani,888,eaae99658bb1a5d14ea89ae89aaf32e0,00904ceec2de7e828c1a5fbe224b1ebf6cb19543f3d24381b64d4329a510e46b,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCSDIUplink.ani,16460,1443e1a4d8c5665e6a88288e1b03e7b0,11de6fad707df80023f91c011569de86705b2770cd1df5bb0394481bf5794fb5,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCSelect.ani,6388,21d58f3402a7f33ca5422fc865d2d602,42840130f31fa90c880ba2855fecff0c4a1cd3e903e6eea862ab95b8dc876427,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCSell.ani,1672,21f1c178ab89de9b364cc8709ad0dce8,3a9dcadef511ad9a2dd1ca9b31eac33a7aa58b652554cd1d18c628c87fab1a4d,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCSniper.ani,15636,29c81451b838097f4baed9b00c3f2873,258ce022aa05087bd0772484d3aceff49b9beb532cd8a8d524f9eb97be57de80,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCSpyDrone.ani,1632,b30aaf4f9ef73831165b1e280050da14,6a4302a4a897d6ded38ee0216bc45975406d40a73270eed12d13cb73c868ba34,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCStop.ani,882,28eb1d384c4ff53a0b44352fd62455b3,ebd5c2968465f5e3898d38d3a737f2bda387be6be6ee246712a5081d703150a1,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCTimedChg.ani,8634,c84d13defbb211c95a8015cfc743320c,727c0863496e777929a86425fcc849e7041b77e3209151f566fe233bf8a2bce6,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCTNTAttack.ani,4744,23569a40a3dbe637a6fdd839d25f58ec,e5c03a381c120e26d04c5c7495c88bf43224f51d89e9ca9d7dea84a5903285fd,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCWaypoint_S.ani,1682,b7566c5d6d3bc8e604f6c07b9dedf4ff,1c585e4c98e05ad6fa916f6d496dcac8673ee651a8cd6743319850e3d14bd4bc,Generals,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCWaypoint.ani,1676,c76b8d01c707588e02aea0c9ae052c54,6335fdebaa2ce98e17fec156d75ff9b089ab0f42b70170e5f18383abc73f1e88,Generals,EN,False,"{""category"":""other""}"
+Data/english/Movies/EA_LOGO.BIK,1596884,30d68ce60d0433e58a77def4f2d6c0fa,ee0f3965f1733ec71a3f60aedc09ae0a53e32e7af9fa97dd5601c2d2ce8aae82,Generals,EN,False,"{""category"":""other""}"
+Data/english/Movies/EA_LOGO640.BIK,839924,00d2b4af0fcba270f0c4d50fdab8e71a,e6f33cf7523f09ac8368badee30b394c3a8f263b8d5aca5c26f2535857c43480,Generals,EN,False,"{""category"":""other""}"
+Data/english/Movies/sizzle_review.bik,19887184,a5da392e70910f56ffa563e8720296d6,9bed2259d7088cc4cfa437e3b0ac54449a2069956e3f033ca37d0c2c88274869,Generals,EN,False,"{""category"":""other""}"
+Data/english/Movies/sizzle_review640.bik,15345648,1b9993508acd143a86d8e0682db4ca99,0709f9a04903ae915eed68e7bf883b51c0fa22b50096ba9c334e4e0b113087a4,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/CHINA_end.bik,21096880,131d68acde339d1f204de92733c2d5ae,63553a1c27e4677e381b222fd711a71f72e8289ceeaf0ac0ac07a90a7b4a883a,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/CHINA_end640.bik,16030972,e9356c5ba8b2f15c8eb05a300851fb50,886ad6a33edff45b0f41e3060904dd4e4df7bc9bbf08933fc86b34a1d47183f0,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/China01_Final_00s.bik,9462664,d2ad7d6128e139113c364d26da6f85e0,a183045298d219626b3681adeb77a26b74ca0ecf7ce3daf4a7aa41a11178e237,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/China02_Final_00s.bik,9416184,eabc0320e25a3214936fe7a664a5ac28,2332508999e8ba62c531d3763c9b2cc5627fa059f7bb504b419f089ef2d8dfff,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/China03_Final_00s.bik,9466800,4983a2ca739a4869f60346855a5e4b85,f6554d863a1e47d374e490161c39200df6786478674a9bb4c4a8542362b3a259,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/China04_Final_00s.bik,9304904,989929b5f0f10a943162e6d7659382cd,b28c8a1f63a5e01df3189d6b537fab52cf7a3ff209d845256f3b9fef21d2315a,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/China05_Final_00s.bik,9434164,e90bb8585ec81807290394c39ab66dca,7c520491d39aa3e50a895e8e34bace4dbdf2cd424b507461b3f1959e3c2224e8,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/China06_Final_00s.bik,9484312,a298248160b6a42b7fc4cfa8e2615dad,e232468337b3573151ffa953c5e357f8fc144951114a8db1441dec41414d3c5a,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/China07_Final_00s.bik,9480224,b1dc505e4e57322c651d5ec8071dbf12,aca8ce932d965cd7514a3d5ec50c6a5bf0673409ab316d6b6a429d1f09e6f8b2,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/GLA_end.bik,18464604,705f46b48604eb83418f51f233d3fa5c,141553b0ff8f27dccd6a36008bf4cf547117da09e3b13de29e56b55a7f3b5bf3,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/GLA_end640.bik,14030116,28bc6598a0816b49270dc3374e2fda7b,266765c6f199a662c3bcb4b9c1bbb6564d25766b77b164c4bc3d014e593375f7,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/GLA01_Final_00s.bik,9380716,9c8328840cab3d83a10cfd9b5f2b2d0d,985d0e9c3e1d2f1e37fc3842827763f584955ca1e929b308f78a92c9d074faf7,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/GLA02_Final_00s.bik,9434596,6fe1a7e3518f9b4d9d4d63cad0b1c243,bf95e38a1d1bc15ef9457b461f80d840a9ad019d45ade6dc2a3070c4a6d24655,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/GLA03_Final_00s.bik,9366612,947d5947b7657072219e9b07137eb678,f9ad9839558a23d34e56a34167773ab4e00ffaf5bcb4b34017408f13449a3fb8,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/GLA04_Final_00s.bik,9479028,ce76409306e2450955170ac4f6cc9ad4,60a3713905f240a645aaa9d28f95141876afd2d2c13db66a3ac6449a0c1aef13,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/GLA05_Final_00s.bik,9476400,438e155a4fdf8b6a337e806eaae946b6,6cb3a3a63b0970b72151d88c8635713bb6c279747fdc829647a98d3b869d9655,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/GLA06_Final_00s.bik,9380596,9b2c80a2d661edc49cabdb2f9f34376f,2a2574025f5dc703026d94f5fb373721cd9440c71ae0a5ac6930a4a3151c36e5,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/GLA07_Final_00s.bik,9314264,9e6f4ca61154a9ce1bf654e8abeec032,613de8f96bca17a350e6f8a2abd902e6d8bfcd6b1f0ceac299baf086d90922aa,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/GLA08_Final_00s.bik,9454488,9c20c612eaccc90324b01b1263b533d9,25ede4b757b4aadae00dcd7fd3e7632b553dc9443894be0363e3f23ed49638be,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/Training_Final_00s.bik,9497308,c97f7aea6379f1abb2fa1352dda02cbf,5a46ab6aa21bb760faf3f6d8172d7ab75c009ad9adc95e01c40ebd81d76aeeb9,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/USA_end.bik,20857148,8b13a3c745df210b2df2147750922414,e2149c34fca288f8f86b43466a4a6aa8cb33a97d4ca8075628d77191c08051cf,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/USA_end640.bik,15464260,49b90621d514e06334573c87fafa9e10,f39bc09b30d23ab47b52fa0c97d07375bcfcc75a5097cd45a23c4f1262891682,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/USA01_Final_00s.bik,9440016,bc0908dbfd543f6f816146f4d4794e2e,416f0acdf3a36b4009389e55e8a27001df336bf40921413892f40e923bd78cf4,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/USA02_Final_00s.bik,9484920,99f59a5108b72b46afd223d3bd3b1439,ce76cc66cbdf911e1e6283673b4c26b38196ee0c9e05274f26e5c92f1e363456,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/USA03_Final_00s.bik,9358748,5367c7faac2a8639cb5101f969c82442,51a7f8c59cb3f4cee9c2af1e670549907eb81e8806a5023db20d6a3c211f8a46,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/USA04_Final_00s.bik,9416664,5f5ecb41dcaeb833dbdc82790ee4f7cf,3b183b059e43c1d1184f7206d0d542d94ae58a736035e12cd943c470038ca5c2,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/USA06_Final_00s.bik,9258892,05880cd511c0658537edb5238e7e6e31,45fdd0345c8461c6ae8eae614e540ccd37b3ce6a2345cc9a7ae0e0dea760b0d9,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/USA07_Final_00s.bik,9466124,31977bea9ee3ebd1c5fa0f5d612074da,ddbbe1be231a7ffdaa3235981bb1936e4c1c5dcd08284f0b262095010970806e,Generals,EN,False,"{""category"":""other""}"
+Data/Movies/USA08_Final_00s.bik,9420052,20c9e77027b4a0e245bcbebaac11fff3,59e6ede70c0f8c0f8557d063356f6ccc61cbc829adb1f619ce9aaec0d4d98eb6,Generals,EN,False,"{""category"":""other""}"
+Data/Scripts/MultiplayerScripts.scb,6915,1c9afbc46dad13f55c317a162255d6ae,86c6a3b82a4188ba3c7abf1388f9d5ee503f06634466be095256a2aefb4ae06b,Generals,EN,False,"{""category"":""other""}"
+Data/Scripts/SkirmishScripts.scb,467525,c8e1e11e697da5adf6abdcdf1290c578,9eedadc0d8d9deb241d56a3db148720029e119c0cfc03a3ef74cade9da3106a5,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust00.tga,16428,9f2d9b6133b6e4d78b409ca8303cab85,d4b2fa073a52734658fed6b780de379fe866b956aec8c95525457232e7d1b636,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust01.tga,16428,5b257180aef56e05b007be93eb64e527,67116bb0c18491cf403d5706e4d6c005d43ea77e034cb889d60465501d504156,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust02.tga,16428,6802068b10f1f8ac64646d54124b7e96,2718869ffb24f789a912b5fe0dcd94067db1a06aa144e1a1ac0d81cd6b5c8fbe,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust03.tga,16428,af760ffba0eff9fe49ace5e8e82de122,6e7c3733f937534800dfc1ac4632c34dd4c873d6d9e55f13dcedf738462d470d,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust04.tga,16428,3068c2e091b0c52c93b0d83d18b04ea3,7123520dc1d22b0279e24f50f4dd5c62d54b2185883acadfd723e19eda7d5c0e,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust05.tga,16428,4e6a03367c3282c939f1002f826c6549,7a44d215ad75268319abcef7cd2ac89dfc68cf7a4ff090cb354db287971764a4,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust06.tga,16428,daefb72301f19803058795c9d4eeb833,c1fba0eef7d968bdd1866fdc13495e0c3ef5f5171f6a6c70f4e73e7097dc6d52,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust07.tga,16428,8b8ccacbcdf9cdef9481cd9428760c44,b84e11ade33a67bce27c7c2dfcbcc569045e41609332f6f62ef12c675db7baa8,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust08.tga,16428,0def5b045f0e0ddabf002d68efb8d347,a1e37542342ecb0d7c482512b3f98195b1f17706a0f1579221fa78f3644d64ba,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust09.tga,16428,f89c4c80fb4d69d23c0755787d18f12b,48608dff4b30b2b25a5fa0c450f744c56e37f007f01b351018c82df7e7198ea4,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust10.tga,16428,44baf98408c4f629a79495ca88669bec,c2e1c7d5594681269641e08442334eee3d0da26b7c385036427dd33aa56ecedb,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust11.tga,16428,db0ad392bd51d1fe89ee050e7e9a430b,1bb9d5b5edd128e315bba2f24ed00be12e4df7ce8ca37f7c7d84ef666e8ac72c,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust12.tga,16428,0416996a4b5ba4c7a60f0456cf1c5a76,dfa05cbcf7d32b8057bf57fa0c7b05e24839205845e66ce06d612130a3f31376,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust13.tga,16428,9016d60dfcd7fa826edd07464a1cc1cb,5baa929913b5ce1a1cbec97f6cd9090dfa3d78ebc0e000d1ee6b8e0f45591255,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust14.tga,16428,deb81cd26d0d67043fd6338c691338f3,29c941490d668adb8cff8927ca5e6694847dffc4198739e88956356e6f2a3094,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust15.tga,16428,d10b07457cc172d05b8508b427e24187,f87addd5360edc1bd5822bcc2818dda27eb727054783b346ddbea26b78e82ff2,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust16.tga,16428,d75dfff45be2c47b5f3c115f9d78bfe2,90dee0bb7198e675772d81e30327531f8bbb535d299b8dd613466c44cd7235f7,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust17.tga,16428,bc83a41cb453dfe12d378ecb8c799d49,f053a13e73c8ce938938feea573141a0e22b50eeaec7f101f4636004f8ba554f,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust18.tga,16428,040c0932fd2705ea0a37e360c57b1cec,c26f3a040e1284a99e06b18953df40efbeabe1e0947783c797b363364ebb99f1,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust19.tga,16428,85e1680cea2bb20578e4f1aa19dfa67e,04915f82f986d72f6c999583b957bb735ba221890bec61aeeb1a16abb1d4352e,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust20.tga,16428,d5ac8f99b83fc92ba4e2f356c6816c21,1a27a58af21b14b8da96728ba62725626085f6b50399d99ff990fd2bcde3e725,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust21.tga,16428,ab1231ec264d2b93cfca0cadc52554f4,8c0e3d19126da509f7cdea23e98142b489479019095ae7cfc00024921523ad5f,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust22.tga,16428,f349cba69868d4da452fa37ec21f860e,fbdbef23579e743b027ce6177e3e15edd3df2794ae49bfc6144c81b777d395a0,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust23.tga,16428,79cb087169f45f2d2b121762b0175b5e,478d7d5e09a60624104647106b05c02d6831a68a7aa14590359d3d00fa02accc,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust24.tga,16428,64e7f0a3fafb344ff8772d2b47aabdb2,e19606c3677d773c9f39681bc0a03c49376292d7f0bc5ce340ccab19c1f09166,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust25.tga,16428,255ca010b8657f14c85bc72028472bf6,b29b4df4052bfde94ce82617f647c02e3864c1f730e9d91f880bb43bbb325f5e,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust26.tga,16428,e79bc7f009b31a48137b5e59fb704ffd,4fd4406232083d148b97cb1de257f5674023c5ecb92b4418291f3ab6667a8c69,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust27.tga,16428,4f7c2763c2f1b4ea4ef99c2bf6496404,becbb96d56fb5a22bc809c2100ee7beecab9470593feec4bf82354d81aeccbc5,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust28.tga,16428,9f399179b32616aebff3a1638973de15,2f0f5c933ff40b9c959c484a317a2e0568392fb2347e76bdbaac2e850e1de642,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust29.tga,16428,cc2998d2d0044e1762d794ed38930cc2,7437ed2c75b3f791372ed914c692e0cb36387d4586089a818c9f33be38a41c32,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust30.tga,16428,88da7064ccdee97387ee50c6c678ce7b,11a82a9b483dcf824e8e86997decc89286e6cc5af71624c1186a186aa5721a35,Generals,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust31.tga,16428,418b9f65207e4d7321c5e87f98f3084d,6b49361ac4c0709a3dbf15e3ec537aa616e0c324bfa6bc490d28cee5c3fa7cd6,Generals,EN,False,"{""category"":""other""}"
+dbghelp.dll.bak,166880,a158e4e4218517c8768d39d8e0a09c0e,bcf8358127a6d44844b9a0dc105d5ae4052a7ec0f38e9516f55125c066e2f89f,Generals,EN,False,"{""category"":""other""}"
+DebugWindow.dll,208964,8c8cd6e54cbc176add4599adf0d4a34e,2936fe750ef3d4d99ff2f7fa6be0ac315ff4c1e6c07b570dba4274f0a7165a6f,Generals,EN,False,"{""category"":""other""}"
+EdgeScroller.exe,576000,5934fcac40a50aa16abb971b5e66508d,acce64e1bc88eb7085baba1669f5f0a73335aa87a387e796a5615794064a4a66,Generals,EN,False,"{""category"":""other""}"
+English.big,2774948,a12512c67dac5bcc81a2be26461001b5,218440f15bd2f718c6897f631eeacb8a42378679bcc0a6aa8a9cf83d0798f543,Generals,EN,False,"{""category"":""other""}"
+game.dat,6101040,1f1e963e0cb91469bf5cbff28c4efd6f,1c96366ff6a99f40863f6bbcfa8bf7622e8df1f80a474201e0e95e37c6416255,Generals,EN,True,"{""category"":""other""}"
+game.dat.bak,12175768,319fe6f63d808293e4a66c7d7bfe81fc,69a39881179112a566cef69573b20065cc868516c49af0761f809ec57da0bdbc,Generals,EN,False,"{""category"":""other""}"
+GameRanger Game Location.lnk,1047,e49a5c4201848f3a2628a1b4261a106b,beeaa2ef5538bb6e70dbaea71284dc0c989a3ca2442b1cc951b54b6e16bb454b,Generals,EN,False,"{""category"":""other""}"
+Generals.dat,56,55734c6663b6187eec266cb102d2d1b5,756b275164a783c1954b849966287df8b1494a12909635d07335c3faa1d2edf6,Generals,EN,False,"{""category"":""other""}"
+Generals.exe,6101040,1f1e963e0cb91469bf5cbff28c4efd6f,1c96366ff6a99f40863f6bbcfa8bf7622e8df1f80a474201e0e95e37c6416255,Generals,EN,False,"{""category"":""other""}"
+generals.exe.bak,4131160,ac52260506ee0e745768b7d70a93e64b,8dde6c990280ac44b4629a664b24bbaf226e629e9c7700234010f198783b6674,Generals,EN,False,"{""category"":""other""}"
+Generals.ico,3998326,046e56495bf47bc51c08284ad4bb587c,9f3d03c9a658f9c7206d30c65dfcf984f0dc8c316d721ccc520d30a67e2c1ce4,Generals,EN,False,"{""category"":""other""}"
+generals.lcf,144,2438db32c55a1551d1daba2e9adc0f40,49ac9bf59939df878d2ad3a130bb2ae6ea6f3c6ada56d0bd35d5aa891853de0f,Generals,EN,False,"{""category"":""other""}"
+GeneralsHD.ico,171327,80f24c7031de8ecb6cb58b22769f0847,373d562fb9b5433e1f2c2988e15852b75550467ab22f171fa78dffe36f2329fd,Generals,EN,False,"{""category"":""other""}"
+gensec.big,787464,20f71781f3dfeb4dc1126b45e5204081,b3baa19899073a3b7ef5fe4347137a09a3f0ef5613175538ccd2b641f9734c9a,Generals,EN,False,"{""category"":""other""}"
+GenToolUpdater.exe,1556992,541fc0780b361c3a701b239fe4e68e7d,77dc12afd049db59194382356ecda1f40fa31fb956c5051f66ab5bea48dca3f2,Generals,EN,False,"{""category"":""other""}"
+gp.info,3,10400c6faf166902b52fb97042f1e0eb,df4e26a04a444901b95afef44e4a96cfae34690fff2ad2c66389c70079cdff2b,Generals,EN,False,"{""category"":""other""}"
+INI.big,7607479,ecdd6e48060398b207f70a6d40917e97,bff8d621088b25fd8b041c8acca020a020fabc66f972ab2bd131fc67d905a72c,Generals,EN,False,"{""category"":""other""}"
+Install_Final.bmp,1440056,ff426b5b7fb72dcb3e0d38846f7ac128,05fe660e4794ce97752e15074ad61f0ef5508484a776be97bd1f0d46f4786baa,Generals,EN,False,"{""category"":""other""}"
+langdata.dat,25398,2e7d20210b21b5fe40e1bb44af63c1c2,b964985085af30ff170d25cdbf97a000db1f52d99e9e4cda293a7761cc5a2616,Generals,EN,False,"{""category"":""other""}"
+launcher.bmp,39608,4ad9b62f4ab1fababf35c616b6e7285f,4d61a1b74377b4a42353d17b10b72a1757f6da4efae3b79a355f38a500051922,Generals,EN,False,"{""category"":""other""}"
+Launcher.txt,22694,134a4bfa70086c13a83d9aed3f7866af,70dac78e491bd1419b47dbc34c09016e403da661e8c2a29134d719bec1f020b6,Generals,EN,False,"{""category"":""other""}"
+launcher/commandline.txt,5,047900114715c73c30ac13a0f794bc70,b67d74fdfbf09721df549f95c107b49dcac6bc890871f29c444ac03c39f847c9,Generals,EN,False,"{""category"":""other""}"
+launcher/Generals.dat,36,60d7d366bf5b398f4062abef84f04eb1,86e8278ee025d252baa894d6828decb6a595000936aa6b084c818d56dd3ad4c6,Generals,EN,False,"{""category"":""other""}"
+launcher/Generals.exe,44032,3c88b33360c53283c66d4909a5e87382,2213d895dee35a7b25e218afcba3626a8e1f2b130e0c2a689f81fdf859068050,Generals,EN,False,"{""category"":""other""}"
+launcher/launch.txt,15,bd3a3d21429c57c6967abd954a202818,5a11487c3afed91096a7f360a7e7ef9aa9ed591884677daa67cce6ee4a5aa92b,Generals,EN,False,"{""category"":""other""}"
+launcher/MapsZH.big,24,a84772702de9c37cdb4844d23838466b,a7bb6186740c0e3be338d58a2699fffd0cbebaab9603f96facf561dc4ddb47d3,Generals,EN,False,"{""category"":""other""}"
+launcher/shellexecute.txt,1,cfcd208495d565ef66e7dff9f98764da,5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9,Generals,EN,False,"{""category"":""other""}"
+Manuals/Generals - English (UK) - Manual.pdf,3755649,90a880bc9fc3db92816d580c797543c2,8a80dc7d396aac32086e7493be79a5d496afec4420224b66878f481dc6e83b10,Generals,EN,False,"{""category"":""other""}"
+Manuals/Generals - French - Addendum.pdf,125774,890498dd7898fc3af01b6c03e2930a18,376367374b876f7cbee9a5b2f84057be7f12401d7edfa46b430b5c96375be9d1,Generals,EN,False,"{""category"":""other""}"
+Manuals/Generals - French - Manual.pdf,1963911,380774522ca91f2ae01d653b68f2a71c,e3b40dd445460718c2fc70d74b3a61801aa0faef7582925d5905f637ee26540e,Generals,EN,False,"{""category"":""other""}"
+Manuals/Generals World Builder - Manual.pdf,767973,446bb8a6ce353e4c15fc038440fa961f,1844c5bcfe08f3f2619557acda3a0e07cd72f21bb82f1d6a0ecfb14038572e9e,Generals,EN,False,"{""category"":""other""}"
+maps.big,23554152,728bf83395aa64dce52d8d27b5f88a30,8a241df0c87ea47f6ad992984ea73dabf4f2ac5f96f8924f30bb0bf5fc4ac4d1,Generals,EN,False,"{""category"":""other""}"
+MSS/mssa3d.m3d,83456,e089ce52b0617a6530069f22e0bdba2a,41ccd5e30475ef7b40e68aa8c5c0ce18e804179fcaa77ba42e6ffa4f438d9a24,Generals,EN,False,"{""category"":""other""}"
+MSS/mssds3d.m3d,70656,85267776d45dbf5475c7d9882f08117c,a2926f4e2a094a99508c05adaf86c5710ad3cff8bbcf247821feb0e6977f547c,Generals,EN,False,"{""category"":""other""}"
+MSS/mssdsp.flt,93696,cb71b1791009eca618e9b1ad4baa4fa9,e035db7c2a4a2378156f096a1450faec425fd8b89bffb886f68c655480bfff52,Generals,EN,False,"{""category"":""other""}"
+MSS/mssdx7.m3d,80896,2727e2671482a55b2f1f16aa88d2780f,e6c928729db1d7c62d684962f4ecfd6bb039504897af53b72b2a66d32f1bc6b0,Generals,EN,False,"{""category"":""other""}"
+MSS/msseax.m3d,103424,788bd950efe89fa5166292bd6729fa62,62e0e34435b9705eedc73660e64564138d8276dcf7b08dfbaac05c592b67e6d3,Generals,EN,False,"{""category"":""other""}"
+MSS/mssmp3.asi,125952,189576dfe55af3b70db7e3e2312cd0fd,121be91fd21c80396cb5cc46c245d9b3f67a26f8cec4d0ebd03f17cd13508b0d,Generals,EN,False,"{""category"":""other""}"
+MSS/mssrsx.m3d,354816,7fae15b559eb91f491a5f75cfa103cd4,f983c72977f19fb7bdfeaec4db1ee1e169a18cc58499452a5bab9fa2447f68ce,Generals,EN,False,"{""category"":""other""}"
+MSS/msssoft.m3d,67072,bdc9ad58ade17dbd939522eee447416f,5dcbf188c30ae1ac6a3d5b7fac4a25e831c9a495683b044f5141c4bdbc83f607,Generals,EN,False,"{""category"":""other""}"
+MSS/mssvoice.asi,197120,3d5342edebe722748ace78c930f4d8a5,72bac1b0d0d3bfcc235a74c06c3fc62043f197a2bd8ebcf8a89652d78f23157b,Generals,EN,False,"{""category"":""other""}"
+mss32.dll,353488,19dbe712b0bfd69502fa11d4e031dcdb,284c0d9b1abcab659777340e10d6f87bfa583a365543856cc72860240ee734aa,Generals,EN,False,"{""category"":""other""}"
+Music.big,158818808,5b65772e6097d206c0fe326452624637,c1e162b8a7575d98d9e20c7ef582e3edc96b35e3d071d821b01c5426d3c55450,Generals,EN,False,"{""category"":""other""}"
+P2XDLL.DLL,522960,f9b2a9f18d648be3f3006afb97bc7d2f,143d938023d1a1f46c3521c9b5a8d6e5b4c322aa73eccd5ef24124b61f596b97,Generals,EN,False,"{""category"":""other""}"
+ParticleEditor.dll,294983,e7d8e10d4edc0780f16fdcc22c92ca7e,270e2685c77ed726e3ff9ccf929feb09b0bfb69ebcdf970c9826cac48a888977,Generals,EN,False,"{""category"":""other""}"
+Patch.big,1462590,7ab5492ae04a8657feb4ec83aae80280,28dc194412f96dc1f66412430cf74f2d89ad0cdabf70d2c8d1179d8e51743494,Generals,EN,False,"{""category"":""other""}"
+Patch.doc,77824,a1a45ed5d72b7b61476c8193880ecd51,8903d4b3431814c34f1cc245fb76d8ce525201ca6c0357d8b1a0e64460c923dd,Generals,EN,False,"{""category"":""other""}"
+PatchData.big,127,0e01ed5090529cee1cbda46a88ca853a,90952433efe55a774ed3f8375b7700b0a16c8206a760b5cdb3d8707a0e66fc0b,Generals,EN,False,"{""category"":""other""}"
+patchget.dat,122880,13282bada64a35b59f9281ab73932ca0,ab1a8576ce19b00bad8988212d54a9f3da1b30c5ceed38807fed315670b3a252,Generals,EN,False,"{""category"":""other""}"
+PatchINI.big,53348,33f75b3ce36fcd22758d56e980a327d0,57562be140bdf95ea1eb6ec0ded08d0049bd876fc5cd9cc0a003c4e4b9506a58,Generals,EN,False,"{""category"":""other""}"
+patchw32.dll,189136,348c837d96dbe31d780ca8e0e19cbb89,51abed3291b9fffc8a80190c17c2eb5738cec6aa00209db500d86acd181f2653,Generals,EN,False,"{""category"":""other""}"
+PatchWindow.big,261072,e3c10d5cf60dd421c2ad00f622a4b7a2,3d95f1fb4d55d1e2acc7e2b51e1c3aaf26402136ba6762c5c96675a97d2c60cd,Generals,EN,False,"{""category"":""other""}"
+PatchWindow.big.bak,261072,e3c10d5cf60dd421c2ad00f622a4b7a2,3d95f1fb4d55d1e2acc7e2b51e1c3aaf26402136ba6762c5c96675a97d2c60cd,Generals,EN,False,"{""category"":""other""}"
+readme.doc,81482,0c6cff18d01b767d69570cea84f2f0fa,57699818c416cd24d6749d52b448eb207632b0bd9912e3da57576be535f82c55,Generals,EN,False,"{""category"":""other""}"
+RedistInstallers/Options_Helper/Options_INI_Helper.exe,639536,e51f185370050a4243ed6d4e10cd947d,5504c8fd96db6925246fd03f3403528d0943fb20afb4ca596e1517699c854105,Generals,EN,False,"{""category"":""other""}"
+RedistInstallers/Options_Helper/Options.ini,47,faa06d15488c0b0c4efbb6e62f519e3a,f4cb51c7ac077a5360d2f38e8629dcca4fe3e1eeb271cea1ac654c06a520a8a4,Generals,EN,False,"{""category"":""other""}"
+shaders.big,1200,b587ef873d3f5cd475b156d4236c1e5e,b982d3a99c8fae32a6d07ab0994274c1754a0fb08f69646f96d698b3986fe2a5,Generals,EN,False,"{""category"":""other""}"
+Speech.big,13479230,0866ad595a5098345653097dca1afda6,5106e92a91b1159fd861d5e4475beb6e7e05b0d5ac43d65520e02d89e39e33d4,Generals,EN,False,"{""category"":""other""}"
+SpeechEnglish.big,269104188,ead55e76d6b86944aed95b94f631c8d6,b48ede709de86437a9cff23bd27586a03f86e2b009e0773816af48496bb10ae2,Generals,EN,False,"{""category"":""other""}"
+SUN.INI,67,d22b1504b88201f35555d3ae8044e0a3,540379d875c20500d63b4cf6c4d5c6c76cbf7785d426190477e91680055acbd0,Generals,EN,False,"{""category"":""other""}"
+Terrain.big,48342856,9fa5a8c692d6122a20032f3a59a70d2c,4c203b31ccbf7f4a41ca0288d3e782a356a0f75f3315c05a83d7364e19f86f71,Generals,EN,False,"{""category"":""other""}"
+Textures.big,333031108,90a934df85a1d628fc79f440068ce0d5,1303e92c57c9cf4e24bf85b342bd58799924565796f3a4aef65dd4a9967aad5b,Generals,EN,False,"{""category"":""other""}"
+W3D.big,184549391,3e1ddef647cf5b590d2290f797401b15,87727b698089cdc32bc378b1746bf315c2a920d5df33cace0d7085e027b67d36,Generals,EN,False,"{""category"":""other""}"
+Window.big,7962589,8c4cd52a88b80c7e1d507e18fa4a45b1,f211c2abe1773806f7e271cc5d23a38ffa3e1f52baf28ca154489d255d3a0041,Generals,EN,False,"{""category"":""other""}"
+Window.big.bak,7962700,6681234d7a863f5f2841efe1b0e3b773,344f830ce00eabc247524b5e8b1305f10fe8c742a667e238d3a9c1c63fbe6479,Generals,EN,False,"{""category"":""other""}"
+WorldBuilder.exe,12794272,aa590bd8e6bd40ef28473e55f7337d9a,21ce4f63731e26bf02fd71d1dc947f513e5e1fc250f9747fc0c30b490f203623,Generals,EN,False,"{""category"":""other""}"
diff --git a/docs/GameInstallationFilesRegistry/README.md b/docs/GameInstallationFilesRegistry/README.md
new file mode 100644
index 00000000..fb7ba0e7
--- /dev/null
+++ b/docs/GameInstallationFilesRegistry/README.md
@@ -0,0 +1,40 @@
+# Game Installation Files Registry
+
+This directory contains the authoritative CSV registries for Command & Conquer Generals and Zero Hour installation validation.
+
+## Structure
+
+- `index.json` - Metadata index for all available CSV registries
+- `Generals-1.08.csv` - File registry for Command & Conquer Generals 1.08
+- `ZeroHour-1.04.csv` - File registry for Command & Conquer Generals: Zero Hour 1.04
+
+## CSV Schema
+
+Each CSV file contains the following columns:
+
+- `relativePath`: Relative path from installation root (e.g., `Data/INI/GameData.ini`)
+- `size`: File size in bytes
+- `md5`: MD5 hash of the file
+- `sha256`: SHA256 hash of the file
+- `gameType`: "Generals" or "ZeroHour"
+- `language`: Language code ("All", "EN", "DE", "FR", "ES", "IT", "KO", "PL", "PT-BR", "ZH-CN", "ZH-TW")
+- `isRequired`: Boolean indicating if file is required for validation
+- `metadata`: JSON metadata (optional)
+
+## Versioning
+
+- New game versions get new CSV files (e.g., `Generals-1.09.csv`)
+- Old CSV files remain for historical reference
+- `index.json` always references the latest active versions
+
+## GitHub Access
+
+Files are accessible via GitHub raw URLs:
+
+- [index.json](https://raw.githubusercontent.com/Community-Outpost/GenHub/main/docs/GameInstallationFilesRegistry/index.json)
+- [Generals-1.08.csv](https://raw.githubusercontent.com/Community-Outpost/GenHub/main/docs/GameInstallationFilesRegistry/Generals-1.08.csv)
+- [ZeroHour-1.04.csv](https://raw.githubusercontent.com/Community-Outpost/GenHub/main/docs/GameInstallationFilesRegistry/ZeroHour-1.04.csv)
+
+## Maintenance
+
+CSVs are generated using the CsvGenerator tool and committed to this directory. The index.json is updated with metadata and checksums after each generation.
diff --git a/docs/GameInstallationFilesRegistry/ZeroHour-1.04.csv b/docs/GameInstallationFilesRegistry/ZeroHour-1.04.csv
new file mode 100644
index 00000000..2268ce63
--- /dev/null
+++ b/docs/GameInstallationFilesRegistry/ZeroHour-1.04.csv
@@ -0,0 +1,374 @@
+relativePath,size,md5,sha256,gameType,language,isRequired,metadata
+!HotkeysLeikezeIndicatorsZH.big,38460754,2961c9d40bf7fd3befbbb237f19ce204,f6ddeb9e09a6e8fec6d865257e0f8c73b9daefbafeb35a9dd09d40fae9a03733,ZeroHour,EN,False,"{""category"":""other""}"
+!HotkeysLeikezeZH.big,937491,4b12fcd3c977bcbb28ac1bf58e3e1704,f5366e546160269ba57bb82be461050b286766159c41154b4b68d3e1e4fa8b01,ZeroHour,EN,False,"{""category"":""other""}"
+00000000.016,153720,614156f3dc3ada5d21a5f0cb60ce5bf1,a85138edc09cd8c51b337674999b9cea16ef1a25259c7f4f7810edb3add55a0b,ZeroHour,EN,False,"{""category"":""other""}"
+00000000.256,308276,5d8160359b90e0dd13d191e87fd84010,6b4db9327a858fd6a8ee77362b7fc0b6457279a57d942d5143fb7a0230e0b7e3,ZeroHour,EN,False,"{""category"":""other""}"
+310_ExpandedLANLobbyMenu.big,104102,0b666d8441ecd4cc96b351013479f80e,86280f21f332e1d47ff06408435a99870a7980c88c270b2fe1aed7219f8fbcfb,ZeroHour,EN,False,"{""category"":""other""}"
+340_ControlBarProArt1080ZH.big,9702796,ebcf82f0251fa8921d291605db8aeff4,555aa820f02b56bc2bf2509dbce2bdc3d5c8bb0a92c65b99926a750519b7035e,ZeroHour,EN,False,"{""category"":""other""}"
+340_ControlBarProData1080ZH.big,1291228,19756a15bdd8f222075d69d847efc298,423896a825f210760a82336d3a515e3b78b461a5e3edee64a7286415763970a6,ZeroHour,EN,False,"{""category"":""other""}"
+340_ControlBarProZH.big,376,85c58bc26fb303bc080c3c0769db9c1c,10837c0d269605d4e0129869d17d7c6221a729692695c851cbed15431751e8ac,ZeroHour,EN,False,"{""category"":""other""}"
+990_DecalsZH.big,7519527,f08ff11bee730f1d4d322736bcbe6408,b9cdd0445b5724aed55421cc2dd7dac97380e83d12728eb6b168a420a9dfea4f,ZeroHour,EN,False,"{""category"":""other""}"
+AudioEnglishZH.big,58326522,5e6d3b22c4dc45b421417feb21a89427,85109b5cb4a5ef75c5fdd1fe1a66957d951f98f028e10e3729dddc2309402914,ZeroHour,EN,False,"{""category"":""other""}"
+AudioZH.big,23965542,03edcec3e410dd2e6bfb99f92087ad39,6fbd05e43491bfd5f56c9250c3f9c30fc3061cd5867cb912c46486fc78f5e8c7,ZeroHour,EN,False,"{""category"":""other""}"
+BINKW32.DLL,358963,e58a20c9e7b342d5ca1f5ba75f1d1108,892a51c4056efcb22297a3b44a3491e3f5888f28b08ed1b17030f24acffedb44,ZeroHour,EN,False,"{""category"":""other""}"
+d3d8.cfg,62,c0a334f632f616330cbb9aa34aeac23c,a54b6a2372db847a779acd4b450ec1ec86410953376dfe13f2fa43c1ea5ef79a,ZeroHour,EN,False,"{""category"":""other""}"
+d3d8.dll,3843584,8a88058ccb6d2512df70b4c1c80e1317,be5276180d04b3de9abd20aeaf2c1f65a2b65c800233ce49d5e77f1ab42441f7,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCAttack_S.ani,1666,548a3180cc4fadcdf669ea455e6e1921,8e1a57bd031bc8565775f78a162eb0b2f81444d464466e57d43838a116b38ab2,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/sccattack.ani,6398,6d899deaefe2228081f28073a9485ade,fa13992a0603390b0c8fa4200cfca7ee9eb0744cb55b87f49aebee55e5711216,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCAttMov_S.ani,1670,3bd9ee4b858660bbf45fa393dc1c04c5,b9c38e1c1a9294b3afcd029162a79bbbfc285770b5e1ff82998a604755ad7e19,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCAttMov.ani,5644,f06b765c53fda761aebe4f8b4045cd08,c6b2ac92c9d8d8a15bd411dc3e3cf6e276b081cef20035ca82647f0168085714,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCCashHack.ani,7896,19efb6c96fb3b30adfcf6d8ad6fa7981,b41565b664457a00d87efbd24b5681155967620df5ae64a54d28aa9780279385,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCEnter_S.ani,2446,f48af1584768ade129a8a6c4f049b452,858479b74fafd998f1496b69edf2e44245a0c2f8bc8dc6c7ecb9b90d62c7370f,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCEnter.ani,2440,20f9bc7c814947b4cec1349c4bb8bfb7,037ab0691bebf9b4a985d194d0619e9fefa0330a07d4c85bb5047c26d10c5787,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCExit.ani,2436,acf7171dfbe1f72d64c9c08a6a190469,590d419191ec781d97fa7e375498eb99270eeccf8d9ee75bb5c3c67e9dcb620a,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCFriendly_S.ani,6358,5578eda24455b8416ac223d29b4ce475,488b83907f2b12c35d1e3d688a3564acd309d4f2afa1185cade157aee40fc238,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCFriendly.ani,6344,9f7a4f16f5cafbf36e865285ec360638,f7b424afb5575067fffe02cf1afdeb53f62f81ee85a12d581e2a76b5ab418652,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCGuard.ani,1616,1b24328b2926ca7bede732be68b4a4c1,fa8c51b348106c3ede587fc0a13deb63aa54a5db38ce8f582f03b761d57f6150,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCHeal.ani,2478,d12a7573375158375a33131339598d51,32997e09311a684687ab8a339d63d3f075eab98def7e8e0c47b4519b9763a695,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCHostile_S.ani,1674,aa715bed9e28d9dd5a13a56b8fec2e90,73f2fb4c8871afbaa49d6a5cbd34c53e4f29cead68f571d7aab5a5b406c893ea,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCHostile.ani,3270,bf03d453be48a42b218c084cbe19e578,78133602f6b1eceb6257895e9eaeadeb07ddbc9e969f6ac69b6bcbe16b5244ed,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCHostile2.ani,8634,f46b0d70e7c29ddb6b24a14dc053fb95,69a82c2f23d5608f039ce47cde2fe955ec1e4dc9971082ef2c24035f04d0738c,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCHostile3.ani,25792,92918392b8969da0d4562b5636df397c,b86c5be45dbf421172de0d12f4f1b237ce1844ad67ce614620490d60a5cbc5b5,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCKnifeAttack.ani,3966,eb68b3991a575c6ee2247c66b9ff01da,922ba292078be4b9f697450bcf68e23b6bcd9344f433b64641c856f15cc040a1,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCMove_S.ani,1664,3b33efc0b64e1b35f07afb8e098d5cf8,0a60c8f9b6da230767f10275ca97a57347d65a39468f953db122e0f93b07793c,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/sccmove.ani,5638,728cd67cd5a9c8c133f4a05d0a7424ea,edcc9a98a24e0e21bd89e45db2fcdf04c5d3ac359616db96ea6c33407df04c82,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCNoAction_S.ani,892,d9612607ce26ba18da3c98187ba8f60a,2cfa77a51eb15ab54359c13c27b926d7b0f948f2c91d4a1928d139d70ceacb02,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCNoAction.ani,886,cc4e179474a670631835d81725830882,116f51bd9073187b7b2fba0ce08bbe3417cd5daacec7b2af6b78a60afd07d6ff,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCNoBomb.ani,886,48237559686d7aaf721d2cb5109efa67,03e4cd887ab8283a8594b4ae54c0f73b159dce2ebf2df069923531de89be0d7a,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCNoEntry_S.ani,1682,121ffb04e7785ef03dba6de7930d5899,2b76be67e3d75d0cc428efb1563c08c0a6f717d95ffdeb1696c70ba446c92c09,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCNoEntry.ani,1676,bf2c1b618dff02c56bed38e7c69b19ba,ab8fed4bf8bf5bf6ff8e79a40bc89afe9c6bc7c13e2b1f5577506bb48d017803,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCNoKnife.ani,886,973eebf5e3893f3e1ce04ebab5f2b5eb,49d8a05655d9af4bf5c6ab3c7f15e2c86dc1037cc2c016c093d22eada35d75f0,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCOutrange.ani,16460,f18891ca6f6be7f2bc8feeeda196ac80,8539c4a441775dd7ccf5c1faf0cb79fdd5fb6c6797f7d7d3ca98768348429741,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCPlace.ani,1680,dc039f3410c790f3666ac7dc611ef801,a149ac8f358c82b6fd89e2e3fef32b6b1d11adcfd846c3560fd388d75c166e0b,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCPlaceBeacon.ani,7078,82c144ddda013cf670954944a79b2196,d32b4336cbe21101cb872fe9e650f0cad9c77f86fdc26286484448c3fc00b8a6,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/sccpointer.ani,884,54199240e4efefa204418c6c973221b2,ad3829b3e6262f8881ad2a3ddc90e7add848ca5b6bb8fda76facd6ea3a98df00,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCRallyPnt_S.ani,1668,1b3d007599e9307b758c28748321c10b,cfb1cd7baed30b94d5b47a659296738376cafeb4dc492dda6b2c9e5de534d1b9,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCRallyPnt.ani,4064,cec0a2074598b34bebe51d14f7783d31,9d15def280ae76cc3aff9f5ae61547f035b9841ed42e647d13d4cae596a2065d,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCRemoteChg.ani,9412,f8a2450fc0f82f28f584af001dbfbc30,d60b49ac06ae56b56fc03e6cbbf6d1bdbf0bc66eaacec621d7cd0caffd300d6d,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCRepair.ani,5640,a7a77357ec53203c1af9164acf6abff7,baaaa22fb2b331660321833659967d8d057cb50ab31cd2b927459ff0c548ecb6,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCResumeC.ani,17970,a5941a513b20dc2165e7ed9fd2cfdf12,cea73ea5cf0dc34d8299d42ec965eaf2c98a7a501ab1bd5599407f3af91f7e85,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/sccscroll0.ani,888,69a1fbc2773ec932f6a5a7fb37f7f12f,0560079fe4782ac89942eb45e94de7011eb1f4ebb39230890e4917d612bc5d01,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/sccscroll1.ani,888,6aacbae0874a6c9aa335e35947a8ebd0,c48c8691866637e0fa9fc98bb40e2d8240370b4cafd7b767c1595b58789eb06c,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/sccscroll2.ani,888,43f6019d566d0aa8759a42d0a5bec5cd,e62fe7da074898004ee7ef81626830fa94332b3a9d4fe8987e948cea0e2ac1f9,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/sccscroll3.ani,888,6f2e50e14ff2776eae76d961a4a5bfc7,f9c2a000c946a6f6bb420d9671c9ba21a9fc14821a5692a5f2e5b4755fdebb8d,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCScroll4.ani,888,a169748d79da826c10d7e5149c7fd41e,ec77136c9768ca3c50216ae92269aa5fae4f29197a13a355c6f92fcf53ecb279,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCScroll5.ani,888,f17a6fc61a7b944bf5eed66d8242d5ca,2ccb9140e23faf6cc98560e545a54b83efce54dd1bfb01ea84119732d0f04433,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCScroll6.ani,888,33618b4ed32fcec63df6fea44b98bac8,46186f3e8e9243edda6e0baf00e0a0fcaaf5e5d9b4d4531bf13393fdda67acc9,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCScroll7.ani,888,eaae99658bb1a5d14ea89ae89aaf32e0,00904ceec2de7e828c1a5fbe224b1ebf6cb19543f3d24381b64d4329a510e46b,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCSDIUplink.ani,16460,1443e1a4d8c5665e6a88288e1b03e7b0,11de6fad707df80023f91c011569de86705b2770cd1df5bb0394481bf5794fb5,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCSelect.ani,6388,21d58f3402a7f33ca5422fc865d2d602,42840130f31fa90c880ba2855fecff0c4a1cd3e903e6eea862ab95b8dc876427,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCSell.ani,1672,21f1c178ab89de9b364cc8709ad0dce8,3a9dcadef511ad9a2dd1ca9b31eac33a7aa58b652554cd1d18c628c87fab1a4d,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCSniper.ani,15636,29c81451b838097f4baed9b00c3f2873,258ce022aa05087bd0772484d3aceff49b9beb532cd8a8d524f9eb97be57de80,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCSpyDrone.ani,1632,b30aaf4f9ef73831165b1e280050da14,6a4302a4a897d6ded38ee0216bc45975406d40a73270eed12d13cb73c868ba34,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCStop.ani,882,28eb1d384c4ff53a0b44352fd62455b3,ebd5c2968465f5e3898d38d3a737f2bda387be6be6ee246712a5081d703150a1,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCTimedChg.ani,8634,c84d13defbb211c95a8015cfc743320c,727c0863496e777929a86425fcc849e7041b77e3209151f566fe233bf8a2bce6,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCTNTAttack.ani,4744,23569a40a3dbe637a6fdd839d25f58ec,e5c03a381c120e26d04c5c7495c88bf43224f51d89e9ca9d7dea84a5903285fd,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCWaypoint_S.ani,1682,b7566c5d6d3bc8e604f6c07b9dedf4ff,1c585e4c98e05ad6fa916f6d496dcac8673ee651a8cd6743319850e3d14bd4bc,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Cursors/SCCWaypoint.ani,1676,c76b8d01c707588e02aea0c9ae052c54,6335fdebaa2ce98e17fec156d75ff9b089ab0f42b70170e5f18383abc73f1e88,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/Comp_AirGen_000.bik,346496,13ea362d7b47cd7247af5e433e31f16b,2d40193202eb94c4b61d504541e914453c72734ab69777f8805c900607d9b44e,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/Comp_AirGen_inv_000.bik,151536,e72d42f551369f6f827009696642e7e2,9eaeb8644884408b0b12f4f635939d013baea7d17813794a8760c81bab10e375,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/Comp_BossGen_000.bik,346964,06e4f22ea5b688b3b8704dd9a2224484,2c5b1fc04a58ab23a140ef5f604370200754db649726fb1f07ed51fd1e34f91a,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/Comp_BossGen_inv_000.bik,346988,26a765a7dbe257ff065b6a1db6c215a9,113b95f527ab763bb48d598095c0dd570dff925f097ea231d3f18b696d06c620,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/Comp_DemolGen_000.bik,346204,79f69494f98c180251dc20d4942d3693,43bff2a3b1b88e986038e497080f153a2aa3cfa8ca858bd4c179433ea71a9820,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/Comp_DemolGen_inv_000.bik,346196,50f505f6a145b77b922c1ce1fc121fec,787436021a38fc4390ef1d9f4672eb05cc5d1da07df81f4989f8bea0289565c8,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/Comp_InfantryGen_000.bik,346660,909cd4a92955a09cfa5a5d757986cba6,c5dec5ffc859340b06b97bc6e3f50ed48d3aee4022808dae26f52dd32566ce25,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/Comp_InfantryGen_inv_000.bik,346644,beb578d242b377c7ad85772888c5f3e6,f9c6a21d40ecce522b25e31e3759b75c5d0b2aa9654e89cb922b8109b1ccd259,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/Comp_LaserGen_000.bik,346720,def7d95800704d912857326956621e34,52c33f472f294b243051d7a8f7e16f84256ef5f83e669ecbd63d6e779cf80289,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/Comp_LaserGen_inv_000.bik,152152,c6b2fd8c8584e726eb22c83ce47f0bbf,4db8ef4958b397ce3ff6d70bd5559fe52dcae55e5cae11ec383ba678c166a134,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/Comp_NukeGen_000.bik,346788,0980302323d3e57b0e8a10a13b2d0484,e42916ce2dbc58c626e8faf4d71ed870d000e0c31e3cacca3eed9a213bf5ccfa,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/Comp_NukeGen_inv_000.bik,346844,8091a036c4013b40914e3e353ec3066b,c2221846962aa329fe184867effd9b58a755b4ef088c799235ce2a6cfeb5a92d,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/Comp_StealthGen_000.bik,346780,a2805a2af305c3dd696034ec3092757e,b98d23db78e1c5fcb4a4c821210e0209aba982f567c5dd1169d75138df8b0ad8,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/Comp_StealthGen_inv_000.bik,346576,95f63b145cfbea47c8b2b43dcc5e6e6c,13367541c8eef647f944b689027de3b9e05527fb0c01fea3db76442e96c5594b,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/Comp_SuperGen_000.bik,346716,c41c7c55a307b7348816c65ab8615318,48f5c1b8e1a4b8385742b9f4186ef3245d081c6d81a57309f9d6dd787adae5e2,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/Comp_SuperGen_inv_000.bik,346760,99f88fab06fbf9450a78a9d769c52a12,4c532ac82135eb891aace70cbad07de9e0dbb8b829b4c855122cf0024fd49c06,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/Comp_TankGen_000.bik,346692,fa21d37a99b75bd6f05999bf8b54e075,d38eda028458e75806d52aad0d8d91a6f9a5de9ecd61ddb41914be89aa06c9ee,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/Comp_TankGen_inv_000.bik,346824,854ca0f56855864f577bdd0e32bf0eb9,39cd6967d21b3480fbd87750052ffe9305adde460a2c66cfdc199b4adec1ac8b,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/Comp_ThraxGen_000.bik,346192,73cb03cab7bd8e822d46f61250865d77,78995480ccf11157e5a65fb553371683de8b6a34dbba7cc756e55fd1bdf39311,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/Comp_ThraxGen_inv_000.bik,346212,c52f4f03b949d321e126104500167a3a,f33251a5fa071bfc0326b4cbe7ba5c423c8de886f98dc7fe22659e247ed99725,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/EA_LOGO.BIK,1596884,30d68ce60d0433e58a77def4f2d6c0fa,ee0f3965f1733ec71a3f60aedc09ae0a53e32e7af9fa97dd5601c2d2ce8aae82,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/EA_LOGO640.BIK,839924,00d2b4af0fcba270f0c4d50fdab8e71a,e6f33cf7523f09ac8368badee30b394c3a8f263b8d5aca5c26f2535857c43480,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/MD_China01_0.bik,18455164,58b447f059cc41672f4463533cedf269,1c46249e9145606f09346dff88616e0ac95d3d40d193d53e134594e4693553b5,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/MD_China02_0.bik,13208784,7ca5f9a4e00e740d8bafbf8a9f7b5a79,09696f151d47db3c8f99fa1f35e862f803e6b1f7b77613a32fc1d674ae589e62,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/MD_China03_0.bik,14858580,abe073e844cda10163b6b3c8f15bcd80,192ca2cb20e4d7f7b37234a61146854dbae50dedf3405f2bfa89c0d304239c2c,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/MD_China04_0.bik,16527788,78e2d3e6562d093e2bb3e906f2cd0a45,43f130f39cded46e84b49bff079f791e8e3566e95037547618da0cb9bb89de7c,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/MD_China05_0.bik,15937612,8de7f4742ea9b8a7e124a664dff9898e,2341a9531021808560fdac241d14ecc2a678e86c013af74161a484cd5a05713c,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/MD_GLA01_0.bik,15523480,40e2374e0e7b19d54ce433ccbb3b2dea,da4e5a0d5aad89aed9b0643d66c6533fac1e127a3f20e37595223797603f2d57,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/MD_GLA02_0.bik,13984196,7b94d3111786099a03ada61269c6e40a,55ab92bf9ed77be8c6ad7c167634714634d2b5ba156e5a276d6cc3fb181a91f6,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/MD_GLA03_0.bik,15558184,ebcb01cfbbd41687a93c5192dcdbdad6,aef36d070fa054a9a595cb3579c93131c2a7f936208b817313d4170c0903665c,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/MD_GLA04_0.bik,13278492,e1e64e5524e143db38565f92ba95a4e6,dc2aec472a42a85d0307855cfb53e374f81f814db1cc25ae005d3d148874ecc6,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/MD_GLA05_0.bik,18456064,609304c5ac3e35097c11c45d99a5d391,90a51d388e7662488b236833382e342a5c77db218ef02709ddb6a7f21c178a3d,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/MD_USA01_0.bik,23077588,ab98eb72484f7380cba94368394a08fc,81810d0e1505611aad402c6ba02e8f18f1224fa5fa7bde40cf769a5d15f3de1d,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/MD_USA02_0.bik,15983780,d63e78fd37fc7b0788a40fedc537c625,01f8b53b61aa9009c4a8b9092ffdac874c2c280023e2a9311fd358e5b455bf4d,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/MD_USA03_0.bik,13706924,921b27dd9f2deb9317a377aa9d3d7f26,5471d66f13206ac39bd435d53a2c924eb4cf38af9e06a22f4ca17e142f885173,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/MD_USA04_0.bik,16062316,0534b9b0ae2407cb501e3c0696b489ea,1a46805c6a117ffc1d14843d5073f5b246924d413cdbd6448141e1b1ed1941ea,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/MD_USA05_0.bik,20291044,a6e969dd759dea02a3b7609d1a5cef38,97abc54134f15db7359d9509dfbde9e6c9d6382edc5979e66da8dc763119f74c,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/sizzle_review.bik,23891876,4238a81d8ccbaf8324bdf1994ddeade7,66a52a06953255390ca584c385e1f5524864ee48d206713577c172a5aacf1ab6,ZeroHour,EN,False,"{""category"":""other""}"
+Data/English/Movies/sizzle_review640.bik,17220424,e464f33ae298e75d3439756819786136,01ba21833f311e39660b704d9fbe88cc4cc60c641d1f0a699035647c8007c3f5,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Movies/GC_Background.bik,149700,bd8fb9e5d3982d9c86c817512bcf17eb,fd997a3b763c8a6e3a5655eaaa9d105b88471487ee639c71cff23bc3d62e8acb,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Movies/VS_small.bik,310128,6b56f1c09cdba7a363c66873ba385cb4,f0e4c07b1041dd535298a42eb64b8f734560e33d76902f340d4e820704baf993,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Scripts/MultiplayerScripts.scb,10381,2938ee364c4fc60f608d223046ae20f6,86d6bd295dd56dc17c6c1289f9a530506c755b0dbc3e868448d93dd468738ab6,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Scripts/Scripts.ini,68743,e37952e22da90832511f99dbcea732f5,2c72d91a77929fd5f2b6f590b4af5ef1c099379bf9c6e39b0f249022a54b7554,ZeroHour,EN,False,"{""category"":""other""}"
+Data/Scripts/SkirmishScripts.scb,2272629,0635cfc535efd6f0a09495326bf5d8e5,8f93862b751f289b052206b87170cc840044cb66660fbf6ae30d5782c1d73776,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust00.tga,16428,9f2d9b6133b6e4d78b409ca8303cab85,d4b2fa073a52734658fed6b780de379fe866b956aec8c95525457232e7d1b636,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust01.tga,16428,5b257180aef56e05b007be93eb64e527,67116bb0c18491cf403d5706e4d6c005d43ea77e034cb889d60465501d504156,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust02.tga,16428,6802068b10f1f8ac64646d54124b7e96,2718869ffb24f789a912b5fe0dcd94067db1a06aa144e1a1ac0d81cd6b5c8fbe,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust03.tga,16428,af760ffba0eff9fe49ace5e8e82de122,6e7c3733f937534800dfc1ac4632c34dd4c873d6d9e55f13dcedf738462d470d,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust04.tga,16428,3068c2e091b0c52c93b0d83d18b04ea3,7123520dc1d22b0279e24f50f4dd5c62d54b2185883acadfd723e19eda7d5c0e,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust05.tga,16428,4e6a03367c3282c939f1002f826c6549,7a44d215ad75268319abcef7cd2ac89dfc68cf7a4ff090cb354db287971764a4,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust06.tga,16428,daefb72301f19803058795c9d4eeb833,c1fba0eef7d968bdd1866fdc13495e0c3ef5f5171f6a6c70f4e73e7097dc6d52,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust07.tga,16428,8b8ccacbcdf9cdef9481cd9428760c44,b84e11ade33a67bce27c7c2dfcbcc569045e41609332f6f62ef12c675db7baa8,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust08.tga,16428,0def5b045f0e0ddabf002d68efb8d347,a1e37542342ecb0d7c482512b3f98195b1f17706a0f1579221fa78f3644d64ba,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust09.tga,16428,f89c4c80fb4d69d23c0755787d18f12b,48608dff4b30b2b25a5fa0c450f744c56e37f007f01b351018c82df7e7198ea4,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust10.tga,16428,44baf98408c4f629a79495ca88669bec,c2e1c7d5594681269641e08442334eee3d0da26b7c385036427dd33aa56ecedb,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust11.tga,16428,db0ad392bd51d1fe89ee050e7e9a430b,1bb9d5b5edd128e315bba2f24ed00be12e4df7ce8ca37f7c7d84ef666e8ac72c,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust12.tga,16428,0416996a4b5ba4c7a60f0456cf1c5a76,dfa05cbcf7d32b8057bf57fa0c7b05e24839205845e66ce06d612130a3f31376,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust13.tga,16428,9016d60dfcd7fa826edd07464a1cc1cb,5baa929913b5ce1a1cbec97f6cd9090dfa3d78ebc0e000d1ee6b8e0f45591255,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust14.tga,16428,deb81cd26d0d67043fd6338c691338f3,29c941490d668adb8cff8927ca5e6694847dffc4198739e88956356e6f2a3094,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust15.tga,16428,d10b07457cc172d05b8508b427e24187,f87addd5360edc1bd5822bcc2818dda27eb727054783b346ddbea26b78e82ff2,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust16.tga,16428,d75dfff45be2c47b5f3c115f9d78bfe2,90dee0bb7198e675772d81e30327531f8bbb535d299b8dd613466c44cd7235f7,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust17.tga,16428,bc83a41cb453dfe12d378ecb8c799d49,f053a13e73c8ce938938feea573141a0e22b50eeaec7f101f4636004f8ba554f,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust18.tga,16428,040c0932fd2705ea0a37e360c57b1cec,c26f3a040e1284a99e06b18953df40efbeabe1e0947783c797b363364ebb99f1,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust19.tga,16428,85e1680cea2bb20578e4f1aa19dfa67e,04915f82f986d72f6c999583b957bb735ba221890bec61aeeb1a16abb1d4352e,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust20.tga,16428,d5ac8f99b83fc92ba4e2f356c6816c21,1a27a58af21b14b8da96728ba62725626085f6b50399d99ff990fd2bcde3e725,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust21.tga,16428,ab1231ec264d2b93cfca0cadc52554f4,8c0e3d19126da509f7cdea23e98142b489479019095ae7cfc00024921523ad5f,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust22.tga,16428,f349cba69868d4da452fa37ec21f860e,fbdbef23579e743b027ce6177e3e15edd3df2794ae49bfc6144c81b777d395a0,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust23.tga,16428,79cb087169f45f2d2b121762b0175b5e,478d7d5e09a60624104647106b05c02d6831a68a7aa14590359d3d00fa02accc,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust24.tga,16428,64e7f0a3fafb344ff8772d2b47aabdb2,e19606c3677d773c9f39681bc0a03c49376292d7f0bc5ce340ccab19c1f09166,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust25.tga,16428,255ca010b8657f14c85bc72028472bf6,b29b4df4052bfde94ce82617f647c02e3864c1f730e9d91f880bb43bbb325f5e,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust26.tga,16428,e79bc7f009b31a48137b5e59fb704ffd,4fd4406232083d148b97cb1de257f5674023c5ecb92b4418291f3ab6667a8c69,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust27.tga,16428,4f7c2763c2f1b4ea4ef99c2bf6496404,becbb96d56fb5a22bc809c2100ee7beecab9470593feec4bf82354d81aeccbc5,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust28.tga,16428,9f399179b32616aebff3a1638973de15,2f0f5c933ff40b9c959c484a317a2e0568392fb2347e76bdbaac2e850e1de642,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust29.tga,16428,cc2998d2d0044e1762d794ed38930cc2,7437ed2c75b3f791372ed914c692e0cb36387d4586089a818c9f33be38a41c32,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust30.tga,16428,88da7064ccdee97387ee50c6c678ce7b,11a82a9b483dcf824e8e86997decc89286e6cc5af71624c1186a186aa5721a35,ZeroHour,EN,False,"{""category"":""other""}"
+Data/WaterPlane/caust31.tga,16428,418b9f65207e4d7321c5e87f98f3084d,6b49361ac4c0709a3dbf15e3ec537aa616e0c324bfa6bc490d28cee5c3fa7cd6,ZeroHour,EN,False,"{""category"":""other""}"
+DebugWindow.dll,208966,2fe70bfc1520fb5c133122ce31108df8,05f04de39ab9f7ae9b479424b3fa3577a590adda164cfca61f72b69e4b0ae896,ZeroHour,EN,False,"{""category"":""other""}"
+EnglishZH.big,80472928,b88cc553608289160da1cc7af4829a82,d3904216ac210a363d9e0e46980c6ef70232046ba713ea137e8a87cbd57a32d6,ZeroHour,EN,False,"{""category"":""other""}"
+game.dat,6883376,2e041916040f72bd08786bc605975970,f37a4929f8d697104e99c2bcf46f8d833122c943afcd87fd077df641d344495b,ZeroHour,EN,True,"{""category"":""other""}"
+game.dat-BRAVO.log,19,3998438c406bb7ed63d9b6e150e8badd,72eef72a979b60e70c59d38a1012c3680235f7d79707c51611430fe92f34f3e1,ZeroHour,EN,False,"{""category"":""other""}"
+game.dat.bak,6545984,bd53837403b5015f6008ef6aca6ecc9d,420fba1dbdc4c14e2418c2b0d3010b9fac6f314eafa1f3a101805b8d98883ea1,ZeroHour,EN,False,"{""category"":""other""}"
+GameRanger Game Location.lnk,1261,a5192796dde7bb7ae99b23f5cd16c5d7,f7bc3a6919f41c4864dcc453ccb3ceaf9df48c2afeaba6be115add07d6cc3e3a,ZeroHour,EN,False,"{""category"":""other""}"
+Generals.dat,56,e8e9b9e7f7c399782ec8b2c6d708ce84,6256f4e5aa8f540f52d7732caf0dc1181928b1afbc98d671b0501079e9b6658e,ZeroHour,EN,False,"{""category"":""other""}"
+Generals.exe,6883376,2e041916040f72bd08786bc605975970,f37a4929f8d697104e99c2bcf46f8d833122c943afcd87fd077df641d344495b,ZeroHour,EN,False,"{""category"":""other""}"
+generals.exe.bak,4303312,47f4807cf6c4a6a506bd11ef5ada6112,7b075b9f0baa9df81651c0c9dd7d8c445454ae1b2452b928f4a1d9332e9ccece,ZeroHour,EN,False,"{""category"":""other""}"
+Generals.ico,3998326,d88da14b354a0b2b843eca44f47da3e1,0f9756d34c3362cd8369d8c22b17d895c4cf047d27b4ae56fd729c2aefc8fc4d,ZeroHour,EN,False,"{""category"":""other""}"
+Generals.lcf,267,9648766e3d377cc7f544fab225c3da15,9b30cc82856514f1af345701f317b84481a6fa877751e7cd71fd5e60c462a790,ZeroHour,EN,False,"{""category"":""other""}"
+generalszh.exe,6443099,14363927a7e340acf59bbcacccd9b706,703f7e2992191e65de4bb791668ff71461967ff2eb51ac6e2bcb872953c73879,ZeroHour,EN,False,"{""category"":""other""}"
+GeneralsZH.ico,410598,a99ba7658a67b6aac593f3961f7e827e,8722336bc703c7f382cbadad3710b5c62ef689942912cc479602a198658c3c99,ZeroHour,EN,False,"{""category"":""other""}"
+gensecZH.big,787464,c375e701ef869d86e936f01df365c49f,ac2aaf5536a2f748dc99178aa431280f26d9d85179e6c2abedf5adfabc971b15,ZeroHour,EN,False,"{""category"":""other""}"
+GenToolUpdater.exe,1556992,541fc0780b361c3a701b239fe4e68e7d,77dc12afd049db59194382356ecda1f40fa31fb956c5051f66ab5bea48dca3f2,ZeroHour,EN,False,"{""category"":""other""}"
+INIZH.big,18764687,aeec332104db082612d162ec41913986,1a6d41a7a2cb31e67ad2f868aca9264ad069c275e0074f8a0d970a336071e9a0,ZeroHour,EN,False,"{""category"":""other""}"
+Install_Final.bmp,1440056,3ae2a6b7a00941e85d979c5842ed8d95,d2bad77cdec7269346dbf2b7c2c06505f220670352bddb242f6a839f0f1f2401,ZeroHour,EN,False,"{""category"":""other""}"
+installScript.vdf,4657,0d4b73f5f442a4d1adf02ca83dc2bceb,3f5f38a6aadafdf853bf31715ef6305f63d90a4d89fb4713a190261dca00c200,ZeroHour,EN,False,"{""category"":""other""}"
+langdata.dat,25398,2e7d20210b21b5fe40e1bb44af63c1c2,b964985085af30ff170d25cdbf97a000db1f52d99e9e4cda293a7761cc5a2616,ZeroHour,EN,False,"{""category"":""other""}"
+launcher.bmp,39604,bb58f29acaeee51836e0261f03ebd576,9c2c1a24c2972f239adf53f96aa53e4e3288bd37d42531fd4d37d39ab84b2df6,ZeroHour,EN,False,"{""category"":""other""}"
+Launcher.txt,13028,ddd7d6df5ad443d3a4bebacf16b225df,8aadc18dc84587cdea6dca1b75a71be3836b0a3dd2259d39c4ff38f57e91d551,ZeroHour,EN,False,"{""category"":""other""}"
+launcher/commandline.txt,5,047900114715c73c30ac13a0f794bc70,b67d74fdfbf09721df549f95c107b49dcac6bc890871f29c444ac03c39f847c9,ZeroHour,EN,False,"{""category"":""other""}"
+launcher/Generals.dat,36,60d7d366bf5b398f4062abef84f04eb1,86e8278ee025d252baa894d6828decb6a595000936aa6b084c818d56dd3ad4c6,ZeroHour,EN,False,"{""category"":""other""}"
+launcher/generals.exe,44032,3c88b33360c53283c66d4909a5e87382,2213d895dee35a7b25e218afcba3626a8e1f2b130e0c2a689f81fdf859068050,ZeroHour,EN,False,"{""category"":""other""}"
+launcher/launch.txt,15,98dc66374395741c7bc48c6aa7063140,e356ca75e6514278be91f4bab93edb9cd9fd4fd4b334d92c0129a88ebf2ccdb9,ZeroHour,EN,False,"{""category"":""other""}"
+launcher/MapsZH.big,24,a84772702de9c37cdb4844d23838466b,a7bb6186740c0e3be338d58a2699fffd0cbebaab9603f96facf561dc4ddb47d3,ZeroHour,EN,False,"{""category"":""other""}"
+launcher/shellexecute.txt,1,cfcd208495d565ef66e7dff9f98764da,5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9,ZeroHour,EN,False,"{""category"":""other""}"
+Manuals/Generals World Builder Manual.pdf,767973,446bb8a6ce353e4c15fc038440fa961f,1844c5bcfe08f3f2619557acda3a0e07cd72f21bb82f1d6a0ecfb14038572e9e,ZeroHour,EN,False,"{""category"":""other""}"
+Manuals/Generals Zero Hour - Deutsch - Addendum.pdf,182930,80a112b7db56b732497377b124e1c154,15e08584ecd08ffa989e40d88ae52acf0fc9c8ebeed643258f8919f80040e592,ZeroHour,EN,False,"{""category"":""other""}"
+Manuals/Generals Zero Hour - Deutsch - Install Guide.pdf,425494,c4540795ca16559c6da59a77bed3a0af,019a822afb70cfb63fe9456cf4ce9162260153cb6370cfe65dfe04f5a1572d0f,ZeroHour,EN,False,"{""category"":""other""}"
+Manuals/Generals Zero Hour - Deutsch - Manual.pdf,1420783,0697442e56333ba1902ba27e42ec0904,ce0167bbd10c8734d23b3f5ce130941ba032cc48aaf6d4f7d14b445b43b77ee7,ZeroHour,EN,False,"{""category"":""other""}"
+Manuals/Generals Zero Hour - English - Addendum.pdf,181350,d2ec067b247efc0edf70a6cde6c24793,e270cc4869d75bbcfd847f9b6786c2bd1ebe3f9f95bd8f869cd62a43eeb564ef,ZeroHour,EN,False,"{""category"":""other""}"
+Manuals/Generals Zero Hour - English - Install Guide.pdf,409468,f8d4e32dc0cded9c34e651228da28603,fb530fd0977df3a60694ca00fe0b2e84c74c513f049be2cf41f8ac433b46e9b0,ZeroHour,EN,False,"{""category"":""other""}"
+Manuals/Generals Zero Hour - English - Manual.pdf,1433349,7582cc8bcf8bf55fcdcbb15ec85876c2,17d2730c216adf0bae89bdf20106c9c18465944b78a8088be926c798df8055c4,ZeroHour,EN,False,"{""category"":""other""}"
+Manuals/Generals Zero Hour - Finnish - Addendum.pdf,137956,3225dc9b92d0d598474e6eaa13fd6ed4,8e09409e300cdca1ace4745fa483b9678e5b79446b3135a089cebbb5e9ace691,ZeroHour,EN,False,"{""category"":""other""}"
+Manuals/Generals Zero Hour - Finnish - Install Guide.pdf,1619993,6335696ff951676e5e5a6530ef132e65,b6dae06194b492398f1ca11fac3bb568208b6085a61ce99516bd19852dab23b9,ZeroHour,EN,False,"{""category"":""other""}"
+Manuals/Generals Zero Hour - Finnish - Manual.pdf,4402238,511611ac7e743fe549e113f1de0fac8b,ca11aff61f01f03cd881079d0c4c0fbf9313f3f8cb09e159caedafa8af3894ff,ZeroHour,EN,False,"{""category"":""other""}"
+Manuals/Generals Zero Hour - French - Addendum.pdf,125774,890498dd7898fc3af01b6c03e2930a18,376367374b876f7cbee9a5b2f84057be7f12401d7edfa46b430b5c96375be9d1,ZeroHour,EN,False,"{""category"":""other""}"
+Manuals/Generals Zero Hour - French - Install Guide.pdf,413583,9db496e2e6733659bf770e0372b6fb6f,f0f2d190fbf641e5ec792518e8a01fb0fdbda8a7f4bc46e2fadd1bbc6fb364ee,ZeroHour,EN,False,"{""category"":""other""}"
+Manuals/Generals Zero Hour - French - Manual.pdf,1442208,be760a7a95f349f28b8d48dd6ad8e2d5,a3df7708bd0a66e7203a1c4d581613228f596bac3347276945e2441c9254acdb,ZeroHour,EN,False,"{""category"":""other""}"
+Manuals/Generals Zero Hour - Italian - Addendum.pdf,120360,ea0c89487c81145b8b6cbd4aab11acf5,2880393773aaece0f24e1e9234cceababeb4371f361131d08c65e94acae154a9,ZeroHour,EN,False,"{""category"":""other""}"
+Manuals/Generals Zero Hour - Italian - Install Guide.pdf,411914,b605909d4b83ac6416ec059ae759b5f5,8aedd284d841abb2e612532e7f3edde2ccca750564e29c0eb719506462fa3523,ZeroHour,EN,False,"{""category"":""other""}"
+Manuals/Generals Zero Hour - Italian - Manual.pdf,1446773,b998d61684a0c512b2c4931fc2d3d4cf,95053005815934ef22343f32baa4319f7d534d3b310e92075e3d7632d4577de6,ZeroHour,EN,False,"{""category"":""other""}"
+Manuals/Generals Zero Hour - Nederlands - Addendum.pdf,114849,1219b00442c3a839e3e2934b7e53135c,dc5a7416aba53560e9bf34e72ec68c12a1a4e58b6c620c4c3a1cb558a8723c2c,ZeroHour,EN,False,"{""category"":""other""}"
+Manuals/Generals Zero Hour - Nederlands - Install Guide.pdf,1342610,18b989d5d623a76f8380a98b2da3c9cd,1be361e4816368d0094c7d421301fc4dbcc84cd4bc72daf435482ceda5a9e6a1,ZeroHour,EN,False,"{""category"":""other""}"
+Manuals/Generals Zero Hour - Nederlands - Manual.pdf,3474740,2200cc359bfd98df84067d874c6f5d33,163cd00c05432365fe341163c8705cbf84da25072f2bd10ba8e65c95e53dd4b0,ZeroHour,EN,False,"{""category"":""other""}"
+Manuals/Generals Zero Hour - Spanish - Addendum.pdf,137830,b37c175c0734d27677618cf6b2cb070f,2b5b177f0a1136a44b504338b3f7a984412f1e1161a9a0f6c333f80f900b1fd1,ZeroHour,EN,False,"{""category"":""other""}"
+Manuals/Generals Zero Hour - Spanish - Install Guide.pdf,136333,5baaddf1caadcea407a720f8528e8e12,f2c30dded1a87d35107bbd8f44497f8ff3fc4f7f1eca26c23153b5ed719e8ebb,ZeroHour,EN,False,"{""category"":""other""}"
+Manuals/Generals Zero Hour - Spanish - Manual.pdf,788502,bd9be30b2c2f8b876c0fbbc861170f9b,d25665a91e028c0f23bfc6cd76743865d4938c89f6aa1fc950d2c36a3aa69b99,ZeroHour,EN,False,"{""category"":""other""}"
+Manuals/Generals Zero Hour - Swedish - Addendum.pdf,131049,da9c8b0bae08be0d0e6aeb2bc988ebdf,3048f32546ec3a534bc51e8a12ae68abc52e6174f37b0ca345c66687dc3f8cb4,ZeroHour,EN,False,"{""category"":""other""}"
+Manuals/Generals Zero Hour - Swedish - Install Guide.pdf,1618786,d65cf85b0b6372f4d6f65d2c233fbc43,f8b3fbedcc1e8f5728a907df61e62a53164776f1fc024bc8dfb30f2c4e08a8f2,ZeroHour,EN,False,"{""category"":""other""}"
+Manuals/Generals Zero Hour - Swedish - Manual.pdf,4393968,42b00486670cf3c803cfaa9c9cf69d9a,0d9fbecc61d34e2416f694d4476ffe7bf16621b3f633308535b9f3f2e9dd7780,ZeroHour,EN,False,"{""category"":""other""}"
+MapsZH.big,39749312,ba7e68e1c67416fa651f74a3f099245a,35cc8947f34f363d69f5045b9ac65f6ee164b8d5a382a1f7af213dbc2a744b96,ZeroHour,EN,False,"{""category"":""other""}"
+MSS/mssa3d.m3d,83456,e089ce52b0617a6530069f22e0bdba2a,41ccd5e30475ef7b40e68aa8c5c0ce18e804179fcaa77ba42e6ffa4f438d9a24,ZeroHour,EN,False,"{""category"":""other""}"
+MSS/mssds3d.m3d,70656,85267776d45dbf5475c7d9882f08117c,a2926f4e2a094a99508c05adaf86c5710ad3cff8bbcf247821feb0e6977f547c,ZeroHour,EN,False,"{""category"":""other""}"
+MSS/mssdsp.flt,93696,cb71b1791009eca618e9b1ad4baa4fa9,e035db7c2a4a2378156f096a1450faec425fd8b89bffb886f68c655480bfff52,ZeroHour,EN,False,"{""category"":""other""}"
+MSS/mssdx7.m3d,80896,2727e2671482a55b2f1f16aa88d2780f,e6c928729db1d7c62d684962f4ecfd6bb039504897af53b72b2a66d32f1bc6b0,ZeroHour,EN,False,"{""category"":""other""}"
+MSS/msseax.m3d,103424,788bd950efe89fa5166292bd6729fa62,62e0e34435b9705eedc73660e64564138d8276dcf7b08dfbaac05c592b67e6d3,ZeroHour,EN,False,"{""category"":""other""}"
+MSS/mssmp3.asi,125952,189576dfe55af3b70db7e3e2312cd0fd,121be91fd21c80396cb5cc46c245d9b3f67a26f8cec4d0ebd03f17cd13508b0d,ZeroHour,EN,False,"{""category"":""other""}"
+MSS/mssrsx.m3d,354816,7fae15b559eb91f491a5f75cfa103cd4,f983c72977f19fb7bdfeaec4db1ee1e169a18cc58499452a5bab9fa2447f68ce,ZeroHour,EN,False,"{""category"":""other""}"
+MSS/msssoft.m3d,67072,bdc9ad58ade17dbd939522eee447416f,5dcbf188c30ae1ac6a3d5b7fac4a25e831c9a495683b044f5141c4bdbc83f607,ZeroHour,EN,False,"{""category"":""other""}"
+MSS/mssvoice.asi,197120,3d5342edebe722748ace78c930f4d8a5,72bac1b0d0d3bfcc235a74c06c3fc62043f197a2bd8ebcf8a89652d78f23157b,ZeroHour,EN,False,"{""category"":""other""}"
+mss32.dll,349696,6400e224b8b44ece59a992e6d8233719,441b290e7dc6334eb5023cd9b7937739298fdd66c104d4c96e5edcf642ae912d,ZeroHour,EN,False,"{""category"":""other""}"
+Music.big,786724,54d9b5b40a97e9770670ec5a9e0cafad,3f2af9c6dcc2b35852556bdc020c95e43c2f43127c50dbff018e12a2bed6116f,ZeroHour,EN,False,"{""category"":""other""}"
+MusicZH.big,34741916,17c178f302bba43ca5c66efa7e14efab,5aa7b8408e5cf61fd53633f6e6310c76d79e74d75dd3286b6e9b9c718d9c5b5e,ZeroHour,EN,False,"{""category"":""other""}"
+P2XDLL.DLL,519168,f8e5e9d283c5f7ca528777ddbb5d6e48,15dad960f53ba3238564a10678b993ddbe9964b4c5033d905337e0bfb79039d2,ZeroHour,EN,False,"{""category"":""other""}"
+ParticleEditor.dll,295015,c187993f5a6920e872b45efe09e30240,6af23580bc17227c1304ab22adb8249a64ef8be6a8e4414382268035243c37b0,ZeroHour,EN,False,"{""category"":""other""}"
+Patch.doc,76288,edab8002e21a75ef8b23e9432abec6ad,b27361d1919cdc6a213560f7daff0ff04d49f6711a377bbf38de25227b1a0c82,ZeroHour,EN,False,"{""category"":""other""}"
+PatchData.big,127,0e01ed5090529cee1cbda46a88ca853a,90952433efe55a774ed3f8375b7700b0a16c8206a760b5cdb3d8707a0e66fc0b,ZeroHour,EN,False,"{""category"":""other""}"
+patchget.dat,122880,cb197b8fa2ba2bde0eb382f862d8e3d5,fd84c2ce09d574653ade6aae0081b3ea033b230e5e841d6c45fd98d10d47a9f7,ZeroHour,EN,False,"{""category"":""other""}"
+PatchINI.big,53717,81fc08cfd4593089a42917d779c5b543,16028d315c8c4d279beed15f1836a8998ff5c9a4d0d621d4a3d37213a0d9fe62,ZeroHour,EN,False,"{""category"":""other""}"
+patchw32.dll,185344,6ec517e866e476401755281837295579,0ec6e25234ad74489eb1890d4de57bb6140bb8196bdc4a5dcac90dd9d16eb2dd,ZeroHour,EN,False,"{""category"":""other""}"
+PatchWindow.big,260792,0d7a0dabdafeac3b6eabd8ee87997aab,0deb9d4b78ae1e12973b9a207a9a5bb1b7f8866ffaef42283c57f24acbc5e2f8,ZeroHour,EN,False,"{""category"":""other""}"
+PatchWindow.big.bak,260792,0d7a0dabdafeac3b6eabd8ee87997aab,0deb9d4b78ae1e12973b9a207a9a5bb1b7f8866ffaef42283c57f24acbc5e2f8,ZeroHour,EN,False,"{""category"":""other""}"
+PatchZH.big,118822,c190bbaa94d8f940259b6bdc9d611deb,450276fbabd19f79dc0143f70fe755e44a99b22c552bc00c5854fc810e615722,ZeroHour,EN,False,"{""category"":""other""}"
+readme.doc,105810,382a297de6bb74c75f1bc148651b5417,07c5c74e7f67caa8178c3d29ad98d929e4fd1f98d28eef75f204db9f231f68e1,ZeroHour,EN,False,"{""category"":""other""}"
+RedistInstallers/Options_Helper/Options_INI_Helper.exe,639536,0351ce3bd1d35cd90c19657766d8df6a,60ea80d81349985f6f0ba47c500fc8b7ba5510aeca58c285904e3f1bd47003fd,ZeroHour,EN,False,"{""category"":""other""}"
+RedistInstallers/Options_Helper/Options.ini,47,faa06d15488c0b0c4efbb6e62f519e3a,f4cb51c7ac077a5360d2f38e8629dcca4fe3e1eeb271cea1ac654c06a520a8a4,ZeroHour,EN,False,"{""category"":""other""}"
+ShadersZH.big,996,299c959957eac0d8348ad96479ea00c8,6246a6906f93261669c8c19e021c8d3484a6f7449bd5e253c8e998c1a9d31d6e,ZeroHour,EN,False,"{""category"":""other""}"
+SpeechEnglishZH.big,254275694,42dece807234bacad9c58328e37af0ce,5b3c8b1819b1ddfeaf6045a4a5dfb9cb47ad16a508dbf17c317e11b9d310a14f,ZeroHour,EN,False,"{""category"":""other""}"
+SpeechZH.big,6174552,f01b53214e75950749c1e1e0d1dfa105,4498097a3c294bc638e72e09e967bb417b0c9b171b4eee95e2c6c0bffef68fa2,ZeroHour,EN,False,"{""category"":""other""}"
+steam_api.dll,265064,5be6351ea71a94ca4334f3211f5eb609,8d36de57cc6436f4e82ee672023f17a7f83a7a55af558582c2c139f83fb33ed0,ZeroHour,EN,False,"{""category"":""other""}"
+steam_appid.txt,9,eebc63293a9b271e0fa92968256632d1,b901f5252fe2c8490f09449c9c0b71b0d857d9155afa0519e6e2e115ee91e0d0,ZeroHour,EN,False,"{""category"":""other""}"
+TerrainZH.big,8660432,cd7d1e946232307d7b1c089e3af8708e,501ef71d12e7a1398800231f2526d47e8e583607f843a2ebcb0c622e26fc73b3,ZeroHour,EN,False,"{""category"":""other""}"
+TexturesZH.big,222753036,8be93eafc51909aec552827000a940f1,aa975400abd70e45e13eacf4ab22505ea322941dfc492097c68245cd35d4b791,ZeroHour,EN,False,"{""category"":""other""}"
+TUC_Credits_Updated.txt,9575,0d403a8290803ed43e381c88b4700763,b965dff896350082b6e848503c37776093ff46547f245201b433034897588d3a,ZeroHour,EN,False,"{""category"":""other""}"
+W3DEnglishZH.big,2696872,31d49330fb96abd8efadb7f713c8e238,f5d164d1e294f26744e2b87ecfbe13e68affaf5ceb4cf12f43005beb8f54defd,ZeroHour,EN,False,"{""category"":""other""}"
+W3DZH.big,189741064,39a344cef260aeb608c49b0739bfafd4,e308c3aeb49d023ffd98b50400f24643a782a68ac4d2e53171c17fc45d73fdc6,ZeroHour,EN,False,"{""category"":""other""}"
+wb_DebugLogFileI.txt,24675,e8994445c3803948512ed3567b91a173,06dbb43f351d46eb28a79fd8c32b013417a5d9e5bb00be939f9ef2a4544f5cd6,ZeroHour,EN,False,"{""category"":""other""}"
+WindowZH.big,8493519,9d9882a2d5e336eae821643e7e1e603b,0da01f3ae6c3f19656c44cff71f916bd76c9e2cd52fa2cc892272bbb6c6323e2,ZeroHour,EN,False,"{""category"":""other""}"
+WindowZH.big.bak,8493653,11ba0050700bd5f91a146acb8c0293bb,68b519f28d012cd297fae2663d431a4b0e6e416aa7bb1a57a79e5500ebcc14b4,ZeroHour,EN,False,"{""category"":""other""}"
+WorldBuilder.exe,13804104,03673b71c85d1f0a16f5911b671ca2a6,e013e92524d1290499c589748eace7b56c4c021a64e5407353a08cd6dbc46ab6,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Audio.big,127940044,c4d80fc87dc13eacd9dbf2a981b194a3,d522df264149e3e27fd46f027548cc38bd60614a43666555219b7b1d45fb3bad,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/AudioEnglish.big,104744592,fc015ddbe16ac6b4d39a85f5612d7233,39d67dba96111178fcceefae2bedb2dc65b55b968741162c714b333c0b0f5f2e,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCAttack_S.ani,1666,548a3180cc4fadcdf669ea455e6e1921,8e1a57bd031bc8565775f78a162eb0b2f81444d464466e57d43838a116b38ab2,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/sccattack.ani,6398,6d899deaefe2228081f28073a9485ade,fa13992a0603390b0c8fa4200cfca7ee9eb0744cb55b87f49aebee55e5711216,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCAttMov_S.ani,1670,3bd9ee4b858660bbf45fa393dc1c04c5,b9c38e1c1a9294b3afcd029162a79bbbfc285770b5e1ff82998a604755ad7e19,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCAttMov.ani,5644,f06b765c53fda761aebe4f8b4045cd08,c6b2ac92c9d8d8a15bd411dc3e3cf6e276b081cef20035ca82647f0168085714,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCCashHack.ani,7896,19efb6c96fb3b30adfcf6d8ad6fa7981,b41565b664457a00d87efbd24b5681155967620df5ae64a54d28aa9780279385,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCEnter_S.ani,2446,f48af1584768ade129a8a6c4f049b452,858479b74fafd998f1496b69edf2e44245a0c2f8bc8dc6c7ecb9b90d62c7370f,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCEnter.ani,2440,20f9bc7c814947b4cec1349c4bb8bfb7,037ab0691bebf9b4a985d194d0619e9fefa0330a07d4c85bb5047c26d10c5787,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCExit.ani,2436,acf7171dfbe1f72d64c9c08a6a190469,590d419191ec781d97fa7e375498eb99270eeccf8d9ee75bb5c3c67e9dcb620a,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCFriendly_S.ani,6358,5578eda24455b8416ac223d29b4ce475,488b83907f2b12c35d1e3d688a3564acd309d4f2afa1185cade157aee40fc238,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCFriendly.ani,6344,9f7a4f16f5cafbf36e865285ec360638,f7b424afb5575067fffe02cf1afdeb53f62f81ee85a12d581e2a76b5ab418652,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCGuard.ani,1616,1b24328b2926ca7bede732be68b4a4c1,fa8c51b348106c3ede587fc0a13deb63aa54a5db38ce8f582f03b761d57f6150,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCHeal.ani,2478,d12a7573375158375a33131339598d51,32997e09311a684687ab8a339d63d3f075eab98def7e8e0c47b4519b9763a695,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCHostile_S.ani,1674,aa715bed9e28d9dd5a13a56b8fec2e90,73f2fb4c8871afbaa49d6a5cbd34c53e4f29cead68f571d7aab5a5b406c893ea,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCHostile.ani,3270,bf03d453be48a42b218c084cbe19e578,78133602f6b1eceb6257895e9eaeadeb07ddbc9e969f6ac69b6bcbe16b5244ed,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCHostile2.ani,8634,f46b0d70e7c29ddb6b24a14dc053fb95,69a82c2f23d5608f039ce47cde2fe955ec1e4dc9971082ef2c24035f04d0738c,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCHostile3.ani,25792,92918392b8969da0d4562b5636df397c,b86c5be45dbf421172de0d12f4f1b237ce1844ad67ce614620490d60a5cbc5b5,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCKnifeAttack.ani,3966,eb68b3991a575c6ee2247c66b9ff01da,922ba292078be4b9f697450bcf68e23b6bcd9344f433b64641c856f15cc040a1,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCMove_S.ani,1664,3b33efc0b64e1b35f07afb8e098d5cf8,0a60c8f9b6da230767f10275ca97a57347d65a39468f953db122e0f93b07793c,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/sccmove.ani,5638,728cd67cd5a9c8c133f4a05d0a7424ea,edcc9a98a24e0e21bd89e45db2fcdf04c5d3ac359616db96ea6c33407df04c82,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCNoAction_S.ani,892,d9612607ce26ba18da3c98187ba8f60a,2cfa77a51eb15ab54359c13c27b926d7b0f948f2c91d4a1928d139d70ceacb02,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCNoAction.ani,886,cc4e179474a670631835d81725830882,116f51bd9073187b7b2fba0ce08bbe3417cd5daacec7b2af6b78a60afd07d6ff,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCNoBomb.ani,886,48237559686d7aaf721d2cb5109efa67,03e4cd887ab8283a8594b4ae54c0f73b159dce2ebf2df069923531de89be0d7a,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCNoEntry_S.ani,1682,121ffb04e7785ef03dba6de7930d5899,2b76be67e3d75d0cc428efb1563c08c0a6f717d95ffdeb1696c70ba446c92c09,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCNoEntry.ani,1676,bf2c1b618dff02c56bed38e7c69b19ba,ab8fed4bf8bf5bf6ff8e79a40bc89afe9c6bc7c13e2b1f5577506bb48d017803,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCNoKnife.ani,886,973eebf5e3893f3e1ce04ebab5f2b5eb,49d8a05655d9af4bf5c6ab3c7f15e2c86dc1037cc2c016c093d22eada35d75f0,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCOutrange.ani,16460,f18891ca6f6be7f2bc8feeeda196ac80,8539c4a441775dd7ccf5c1faf0cb79fdd5fb6c6797f7d7d3ca98768348429741,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCPlace.ani,1680,dc039f3410c790f3666ac7dc611ef801,a149ac8f358c82b6fd89e2e3fef32b6b1d11adcfd846c3560fd388d75c166e0b,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCPlaceBeacon.ani,7078,82c144ddda013cf670954944a79b2196,d32b4336cbe21101cb872fe9e650f0cad9c77f86fdc26286484448c3fc00b8a6,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/sccpointer.ani,884,54199240e4efefa204418c6c973221b2,ad3829b3e6262f8881ad2a3ddc90e7add848ca5b6bb8fda76facd6ea3a98df00,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCRallyPnt_S.ani,1668,1b3d007599e9307b758c28748321c10b,cfb1cd7baed30b94d5b47a659296738376cafeb4dc492dda6b2c9e5de534d1b9,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCRallyPnt.ani,4064,cec0a2074598b34bebe51d14f7783d31,9d15def280ae76cc3aff9f5ae61547f035b9841ed42e647d13d4cae596a2065d,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCRemoteChg.ani,9412,f8a2450fc0f82f28f584af001dbfbc30,d60b49ac06ae56b56fc03e6cbbf6d1bdbf0bc66eaacec621d7cd0caffd300d6d,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCRepair.ani,5640,a7a77357ec53203c1af9164acf6abff7,baaaa22fb2b331660321833659967d8d057cb50ab31cd2b927459ff0c548ecb6,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCResumeC.ani,17970,a5941a513b20dc2165e7ed9fd2cfdf12,cea73ea5cf0dc34d8299d42ec965eaf2c98a7a501ab1bd5599407f3af91f7e85,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/sccscroll0.ani,888,69a1fbc2773ec932f6a5a7fb37f7f12f,0560079fe4782ac89942eb45e94de7011eb1f4ebb39230890e4917d612bc5d01,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/sccscroll1.ani,888,6aacbae0874a6c9aa335e35947a8ebd0,c48c8691866637e0fa9fc98bb40e2d8240370b4cafd7b767c1595b58789eb06c,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/sccscroll2.ani,888,43f6019d566d0aa8759a42d0a5bec5cd,e62fe7da074898004ee7ef81626830fa94332b3a9d4fe8987e948cea0e2ac1f9,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/sccscroll3.ani,888,6f2e50e14ff2776eae76d961a4a5bfc7,f9c2a000c946a6f6bb420d9671c9ba21a9fc14821a5692a5f2e5b4755fdebb8d,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCScroll4.ani,888,a169748d79da826c10d7e5149c7fd41e,ec77136c9768ca3c50216ae92269aa5fae4f29197a13a355c6f92fcf53ecb279,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCScroll5.ani,888,f17a6fc61a7b944bf5eed66d8242d5ca,2ccb9140e23faf6cc98560e545a54b83efce54dd1bfb01ea84119732d0f04433,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCScroll6.ani,888,33618b4ed32fcec63df6fea44b98bac8,46186f3e8e9243edda6e0baf00e0a0fcaaf5e5d9b4d4531bf13393fdda67acc9,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCScroll7.ani,888,eaae99658bb1a5d14ea89ae89aaf32e0,00904ceec2de7e828c1a5fbe224b1ebf6cb19543f3d24381b64d4329a510e46b,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCSDIUplink.ani,16460,1443e1a4d8c5665e6a88288e1b03e7b0,11de6fad707df80023f91c011569de86705b2770cd1df5bb0394481bf5794fb5,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCSelect.ani,6388,21d58f3402a7f33ca5422fc865d2d602,42840130f31fa90c880ba2855fecff0c4a1cd3e903e6eea862ab95b8dc876427,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCSell.ani,1672,21f1c178ab89de9b364cc8709ad0dce8,3a9dcadef511ad9a2dd1ca9b31eac33a7aa58b652554cd1d18c628c87fab1a4d,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCSniper.ani,15636,29c81451b838097f4baed9b00c3f2873,258ce022aa05087bd0772484d3aceff49b9beb532cd8a8d524f9eb97be57de80,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCSpyDrone.ani,1632,b30aaf4f9ef73831165b1e280050da14,6a4302a4a897d6ded38ee0216bc45975406d40a73270eed12d13cb73c868ba34,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCStop.ani,882,28eb1d384c4ff53a0b44352fd62455b3,ebd5c2968465f5e3898d38d3a737f2bda387be6be6ee246712a5081d703150a1,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCTimedChg.ani,8634,c84d13defbb211c95a8015cfc743320c,727c0863496e777929a86425fcc849e7041b77e3209151f566fe233bf8a2bce6,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCTNTAttack.ani,4744,23569a40a3dbe637a6fdd839d25f58ec,e5c03a381c120e26d04c5c7495c88bf43224f51d89e9ca9d7dea84a5903285fd,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCWaypoint_S.ani,1682,b7566c5d6d3bc8e604f6c07b9dedf4ff,1c585e4c98e05ad6fa916f6d496dcac8673ee651a8cd6743319850e3d14bd4bc,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Cursors/SCCWaypoint.ani,1676,c76b8d01c707588e02aea0c9ae052c54,6335fdebaa2ce98e17fec156d75ff9b089ab0f42b70170e5f18383abc73f1e88,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/CHINA_end.bik,21096880,131d68acde339d1f204de92733c2d5ae,63553a1c27e4677e381b222fd711a71f72e8289ceeaf0ac0ac07a90a7b4a883a,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/CHINA_end640.bik,16030972,e9356c5ba8b2f15c8eb05a300851fb50,886ad6a33edff45b0f41e3060904dd4e4df7bc9bbf08933fc86b34a1d47183f0,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/China01_Final_00s.bik,9462664,d2ad7d6128e139113c364d26da6f85e0,a183045298d219626b3681adeb77a26b74ca0ecf7ce3daf4a7aa41a11178e237,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/China02_Final_00s.bik,9416184,eabc0320e25a3214936fe7a664a5ac28,2332508999e8ba62c531d3763c9b2cc5627fa059f7bb504b419f089ef2d8dfff,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/China03_Final_00s.bik,9466800,4983a2ca739a4869f60346855a5e4b85,f6554d863a1e47d374e490161c39200df6786478674a9bb4c4a8542362b3a259,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/China04_Final_00s.bik,9304904,989929b5f0f10a943162e6d7659382cd,b28c8a1f63a5e01df3189d6b537fab52cf7a3ff209d845256f3b9fef21d2315a,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/China05_Final_00s.bik,9434164,e90bb8585ec81807290394c39ab66dca,7c520491d39aa3e50a895e8e34bace4dbdf2cd424b507461b3f1959e3c2224e8,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/China06_Final_00s.bik,9484312,a298248160b6a42b7fc4cfa8e2615dad,e232468337b3573151ffa953c5e357f8fc144951114a8db1441dec41414d3c5a,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/China07_Final_00s.bik,9480224,b1dc505e4e57322c651d5ec8071dbf12,aca8ce932d965cd7514a3d5ec50c6a5bf0673409ab316d6b6a429d1f09e6f8b2,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/GLA_end.bik,18464604,705f46b48604eb83418f51f233d3fa5c,141553b0ff8f27dccd6a36008bf4cf547117da09e3b13de29e56b55a7f3b5bf3,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/GLA_end640.bik,14030116,28bc6598a0816b49270dc3374e2fda7b,266765c6f199a662c3bcb4b9c1bbb6564d25766b77b164c4bc3d014e593375f7,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/GLA01_Final_00s.bik,9380716,9c8328840cab3d83a10cfd9b5f2b2d0d,985d0e9c3e1d2f1e37fc3842827763f584955ca1e929b308f78a92c9d074faf7,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/GLA02_Final_00s.bik,9434596,6fe1a7e3518f9b4d9d4d63cad0b1c243,bf95e38a1d1bc15ef9457b461f80d840a9ad019d45ade6dc2a3070c4a6d24655,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/GLA03_Final_00s.bik,9366612,947d5947b7657072219e9b07137eb678,f9ad9839558a23d34e56a34167773ab4e00ffaf5bcb4b34017408f13449a3fb8,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/GLA04_Final_00s.bik,9479028,ce76409306e2450955170ac4f6cc9ad4,60a3713905f240a645aaa9d28f95141876afd2d2c13db66a3ac6449a0c1aef13,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/GLA05_Final_00s.bik,9476400,438e155a4fdf8b6a337e806eaae946b6,6cb3a3a63b0970b72151d88c8635713bb6c279747fdc829647a98d3b869d9655,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/GLA06_Final_00s.bik,9380596,9b2c80a2d661edc49cabdb2f9f34376f,2a2574025f5dc703026d94f5fb373721cd9440c71ae0a5ac6930a4a3151c36e5,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/GLA07_Final_00s.bik,9314264,9e6f4ca61154a9ce1bf654e8abeec032,613de8f96bca17a350e6f8a2abd902e6d8bfcd6b1f0ceac299baf086d90922aa,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/GLA08_Final_00s.bik,9454488,9c20c612eaccc90324b01b1263b533d9,25ede4b757b4aadae00dcd7fd3e7632b553dc9443894be0363e3f23ed49638be,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/Training_Final_00s.bik,9497308,c97f7aea6379f1abb2fa1352dda02cbf,5a46ab6aa21bb760faf3f6d8172d7ab75c009ad9adc95e01c40ebd81d76aeeb9,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/USA_end.bik,20857148,8b13a3c745df210b2df2147750922414,e2149c34fca288f8f86b43466a4a6aa8cb33a97d4ca8075628d77191c08051cf,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/USA_end640.bik,15464260,49b90621d514e06334573c87fafa9e10,f39bc09b30d23ab47b52fa0c97d07375bcfcc75a5097cd45a23c4f1262891682,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/USA01_Final_00s.bik,9440016,bc0908dbfd543f6f816146f4d4794e2e,416f0acdf3a36b4009389e55e8a27001df336bf40921413892f40e923bd78cf4,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/USA02_Final_00s.bik,9484920,99f59a5108b72b46afd223d3bd3b1439,ce76cc66cbdf911e1e6283673b4c26b38196ee0c9e05274f26e5c92f1e363456,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/USA03_Final_00s.bik,9358748,5367c7faac2a8639cb5101f969c82442,51a7f8c59cb3f4cee9c2af1e670549907eb81e8806a5023db20d6a3c211f8a46,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/USA04_Final_00s.bik,9416664,5f5ecb41dcaeb833dbdc82790ee4f7cf,3b183b059e43c1d1184f7206d0d542d94ae58a736035e12cd943c470038ca5c2,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/USA06_Final_00s.bik,9258892,05880cd511c0658537edb5238e7e6e31,45fdd0345c8461c6ae8eae614e540ccd37b3ce6a2345cc9a7ae0e0dea760b0d9,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/USA07_Final_00s.bik,9466124,31977bea9ee3ebd1c5fa0f5d612074da,ddbbe1be231a7ffdaa3235981bb1936e4c1c5dcd08284f0b262095010970806e,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Movies/USA08_Final_00s.bik,9420052,20c9e77027b4a0e245bcbebaac11fff3,59e6ede70c0f8c0f8557d063356f6ccc61cbc829adb1f619ce9aaec0d4d98eb6,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Scripts/MultiplayerScripts.scb,6915,1c9afbc46dad13f55c317a162255d6ae,86c6a3b82a4188ba3c7abf1388f9d5ee503f06634466be095256a2aefb4ae06b,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/Scripts/SkirmishScripts.scb,467525,c8e1e11e697da5adf6abdcdf1290c578,9eedadc0d8d9deb241d56a3db148720029e119c0cfc03a3ef74cade9da3106a5,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust00.tga,16428,9f2d9b6133b6e4d78b409ca8303cab85,d4b2fa073a52734658fed6b780de379fe866b956aec8c95525457232e7d1b636,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust01.tga,16428,5b257180aef56e05b007be93eb64e527,67116bb0c18491cf403d5706e4d6c005d43ea77e034cb889d60465501d504156,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust02.tga,16428,6802068b10f1f8ac64646d54124b7e96,2718869ffb24f789a912b5fe0dcd94067db1a06aa144e1a1ac0d81cd6b5c8fbe,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust03.tga,16428,af760ffba0eff9fe49ace5e8e82de122,6e7c3733f937534800dfc1ac4632c34dd4c873d6d9e55f13dcedf738462d470d,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust04.tga,16428,3068c2e091b0c52c93b0d83d18b04ea3,7123520dc1d22b0279e24f50f4dd5c62d54b2185883acadfd723e19eda7d5c0e,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust05.tga,16428,4e6a03367c3282c939f1002f826c6549,7a44d215ad75268319abcef7cd2ac89dfc68cf7a4ff090cb354db287971764a4,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust06.tga,16428,daefb72301f19803058795c9d4eeb833,c1fba0eef7d968bdd1866fdc13495e0c3ef5f5171f6a6c70f4e73e7097dc6d52,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust07.tga,16428,8b8ccacbcdf9cdef9481cd9428760c44,b84e11ade33a67bce27c7c2dfcbcc569045e41609332f6f62ef12c675db7baa8,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust08.tga,16428,0def5b045f0e0ddabf002d68efb8d347,a1e37542342ecb0d7c482512b3f98195b1f17706a0f1579221fa78f3644d64ba,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust09.tga,16428,f89c4c80fb4d69d23c0755787d18f12b,48608dff4b30b2b25a5fa0c450f744c56e37f007f01b351018c82df7e7198ea4,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust10.tga,16428,44baf98408c4f629a79495ca88669bec,c2e1c7d5594681269641e08442334eee3d0da26b7c385036427dd33aa56ecedb,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust11.tga,16428,db0ad392bd51d1fe89ee050e7e9a430b,1bb9d5b5edd128e315bba2f24ed00be12e4df7ce8ca37f7c7d84ef666e8ac72c,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust12.tga,16428,0416996a4b5ba4c7a60f0456cf1c5a76,dfa05cbcf7d32b8057bf57fa0c7b05e24839205845e66ce06d612130a3f31376,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust13.tga,16428,9016d60dfcd7fa826edd07464a1cc1cb,5baa929913b5ce1a1cbec97f6cd9090dfa3d78ebc0e000d1ee6b8e0f45591255,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust14.tga,16428,deb81cd26d0d67043fd6338c691338f3,29c941490d668adb8cff8927ca5e6694847dffc4198739e88956356e6f2a3094,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust15.tga,16428,d10b07457cc172d05b8508b427e24187,f87addd5360edc1bd5822bcc2818dda27eb727054783b346ddbea26b78e82ff2,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust16.tga,16428,d75dfff45be2c47b5f3c115f9d78bfe2,90dee0bb7198e675772d81e30327531f8bbb535d299b8dd613466c44cd7235f7,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust17.tga,16428,bc83a41cb453dfe12d378ecb8c799d49,f053a13e73c8ce938938feea573141a0e22b50eeaec7f101f4636004f8ba554f,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust18.tga,16428,040c0932fd2705ea0a37e360c57b1cec,c26f3a040e1284a99e06b18953df40efbeabe1e0947783c797b363364ebb99f1,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust19.tga,16428,85e1680cea2bb20578e4f1aa19dfa67e,04915f82f986d72f6c999583b957bb735ba221890bec61aeeb1a16abb1d4352e,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust20.tga,16428,d5ac8f99b83fc92ba4e2f356c6816c21,1a27a58af21b14b8da96728ba62725626085f6b50399d99ff990fd2bcde3e725,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust21.tga,16428,ab1231ec264d2b93cfca0cadc52554f4,8c0e3d19126da509f7cdea23e98142b489479019095ae7cfc00024921523ad5f,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust22.tga,16428,f349cba69868d4da452fa37ec21f860e,fbdbef23579e743b027ce6177e3e15edd3df2794ae49bfc6144c81b777d395a0,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust23.tga,16428,79cb087169f45f2d2b121762b0175b5e,478d7d5e09a60624104647106b05c02d6831a68a7aa14590359d3d00fa02accc,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust24.tga,16428,64e7f0a3fafb344ff8772d2b47aabdb2,e19606c3677d773c9f39681bc0a03c49376292d7f0bc5ce340ccab19c1f09166,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust25.tga,16428,255ca010b8657f14c85bc72028472bf6,b29b4df4052bfde94ce82617f647c02e3864c1f730e9d91f880bb43bbb325f5e,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust26.tga,16428,e79bc7f009b31a48137b5e59fb704ffd,4fd4406232083d148b97cb1de257f5674023c5ecb92b4418291f3ab6667a8c69,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust27.tga,16428,4f7c2763c2f1b4ea4ef99c2bf6496404,becbb96d56fb5a22bc809c2100ee7beecab9470593feec4bf82354d81aeccbc5,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust28.tga,16428,9f399179b32616aebff3a1638973de15,2f0f5c933ff40b9c959c484a317a2e0568392fb2347e76bdbaac2e850e1de642,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust29.tga,16428,cc2998d2d0044e1762d794ed38930cc2,7437ed2c75b3f791372ed914c692e0cb36387d4586089a818c9f33be38a41c32,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust30.tga,16428,88da7064ccdee97387ee50c6c678ce7b,11a82a9b483dcf824e8e86997decc89286e6cc5af71624c1186a186aa5721a35,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Data/WaterPlane/caust31.tga,16428,418b9f65207e4d7321c5e87f98f3084d,6b49361ac4c0709a3dbf15e3ec537aa616e0c324bfa6bc490d28cee5c3fa7cd6,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/English.big,2774948,a12512c67dac5bcc81a2be26461001b5,218440f15bd2f718c6897f631eeacb8a42378679bcc0a6aa8a9cf83d0798f543,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/gensec.big,787464,20f71781f3dfeb4dc1126b45e5204081,b3baa19899073a3b7ef5fe4347137a09a3f0ef5613175538ccd2b641f9734c9a,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/INI.big,7607479,ecdd6e48060398b207f70a6d40917e97,bff8d621088b25fd8b041c8acca020a020fabc66f972ab2bd131fc67d905a72c,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/maps.big,23554152,728bf83395aa64dce52d8d27b5f88a30,8a241df0c87ea47f6ad992984ea73dabf4f2ac5f96f8924f30bb0bf5fc4ac4d1,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/MSS/mssa3d.m3d,83456,e089ce52b0617a6530069f22e0bdba2a,41ccd5e30475ef7b40e68aa8c5c0ce18e804179fcaa77ba42e6ffa4f438d9a24,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/MSS/mssds3d.m3d,70656,85267776d45dbf5475c7d9882f08117c,a2926f4e2a094a99508c05adaf86c5710ad3cff8bbcf247821feb0e6977f547c,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/MSS/mssdsp.flt,93696,cb71b1791009eca618e9b1ad4baa4fa9,e035db7c2a4a2378156f096a1450faec425fd8b89bffb886f68c655480bfff52,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/MSS/mssdx7.m3d,80896,2727e2671482a55b2f1f16aa88d2780f,e6c928729db1d7c62d684962f4ecfd6bb039504897af53b72b2a66d32f1bc6b0,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/MSS/msseax.m3d,103424,788bd950efe89fa5166292bd6729fa62,62e0e34435b9705eedc73660e64564138d8276dcf7b08dfbaac05c592b67e6d3,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/MSS/mssmp3.asi,125952,189576dfe55af3b70db7e3e2312cd0fd,121be91fd21c80396cb5cc46c245d9b3f67a26f8cec4d0ebd03f17cd13508b0d,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/MSS/mssrsx.m3d,354816,7fae15b559eb91f491a5f75cfa103cd4,f983c72977f19fb7bdfeaec4db1ee1e169a18cc58499452a5bab9fa2447f68ce,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/MSS/msssoft.m3d,67072,bdc9ad58ade17dbd939522eee447416f,5dcbf188c30ae1ac6a3d5b7fac4a25e831c9a495683b044f5141c4bdbc83f607,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/MSS/mssvoice.asi,197120,3d5342edebe722748ace78c930f4d8a5,72bac1b0d0d3bfcc235a74c06c3fc62043f197a2bd8ebcf8a89652d78f23157b,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Music.big,158818808,5b65772e6097d206c0fe326452624637,c1e162b8a7575d98d9e20c7ef582e3edc96b35e3d071d821b01c5426d3c55450,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Patch.big,1462590,7ab5492ae04a8657feb4ec83aae80280,28dc194412f96dc1f66412430cf74f2d89ad0cdabf70d2c8d1179d8e51743494,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/shaders.big,1200,b587ef873d3f5cd475b156d4236c1e5e,b982d3a99c8fae32a6d07ab0994274c1754a0fb08f69646f96d698b3986fe2a5,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Speech.big,13479230,0866ad595a5098345653097dca1afda6,5106e92a91b1159fd861d5e4475beb6e7e05b0d5ac43d65520e02d89e39e33d4,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/SpeechEnglish.big,269104188,ead55e76d6b86944aed95b94f631c8d6,b48ede709de86437a9cff23bd27586a03f86e2b009e0773816af48496bb10ae2,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Terrain.big,48342856,9fa5a8c692d6122a20032f3a59a70d2c,4c203b31ccbf7f4a41ca0288d3e782a356a0f75f3315c05a83d7364e19f86f71,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Textures.big,333031108,90a934df85a1d628fc79f440068ce0d5,1303e92c57c9cf4e24bf85b342bd58799924565796f3a4aef65dd4a9967aad5b,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/W3D.big,184549391,3e1ddef647cf5b590d2290f797401b15,87727b698089cdc32bc378b1746bf315c2a920d5df33cace0d7085e027b67d36,ZeroHour,EN,False,"{""category"":""other""}"
+ZH_Generals/Window.big,7962700,6681234d7a863f5f2841efe1b0e3b773,344f830ce00eabc247524b5e8b1305f10fe8c742a667e238d3a9c1c63fbe6479,ZeroHour,EN,False,"{""category"":""other""}"
diff --git a/docs/GameInstallationFilesRegistry/index.json b/docs/GameInstallationFilesRegistry/index.json
new file mode 100644
index 00000000..1ed230f9
--- /dev/null
+++ b/docs/GameInstallationFilesRegistry/index.json
@@ -0,0 +1,43 @@
+{
+ "Version": "1.0.0",
+ "LastUpdated": "2025-09-18T03:23:59.2539232Z",
+ "Description": "CSV registry metadata for Command & Conquer Generals and Zero Hour installation validation",
+ "Registries": [
+ {
+ "Id": "generals-1.08",
+ "GameType": "Generals",
+ "Version": "1.08",
+ "Url": "https://raw.githubusercontent.com/Community-Outpost/GenHub/main/docs/GameInstallationFilesRegistry/Generals-1.08.csv",
+ "FileCount": 198,
+ "TotalSizeBytes": 1740480868,
+ "Languages": [
+ "EN"
+ ],
+ "Checksum": {
+ "Md5": "6f4daf1bac18e9ba0d4f5af3780914da",
+ "Sha256": "4d598e8ea86645d86f0b37aa7b944edf3fd63152c341a4edf2c4b21ae87c52aa"
+ },
+ "GeneratedAt": "2025-09-18T03:23:59.2494267Z",
+ "GeneratorVersion": "1.0.0",
+ "IsActive": true
+ },
+ {
+ "Id": "zerohour-1.04",
+ "GameType": "ZeroHour",
+ "Version": "1.04",
+ "Url": "https://raw.githubusercontent.com/Community-Outpost/GenHub/main/docs/GameInstallationFilesRegistry/ZeroHour-1.04.csv",
+ "FileCount": 373,
+ "TotalSizeBytes": 3013490205,
+ "Languages": [
+ "EN"
+ ],
+ "Checksum": {
+ "Md5": "9c0bec173c8fe56c26623d7e19221b2c",
+ "Sha256": "afae863760d34f09907c524a3ff24fb9b225667bfc7c17db7c37aff8dc6a5373"
+ },
+ "GeneratedAt": "2025-09-18T03:23:59.2538477Z",
+ "GeneratorVersion": "1.0.0",
+ "IsActive": true
+ }
+ ]
+}
\ No newline at end of file