Skip to content
10 changes: 10 additions & 0 deletions GenHub/GenHub.Core/Constants/LanguageFilePatterns.cs
Original file line number Diff line number Diff line change
Expand Up @@ -219,4 +219,14 @@ public static class LanguageFilePatterns
/// File pattern for Portuguese ZH BIG files: "PortugueseZH.big".
/// </summary>
public const string PortugueseZHBig = "PortugueseZH.big";

/// <summary>
/// File pattern for Zero Hour audio BIG files: "AudioZH.big".
/// </summary>
public const string AudioZHBig = "AudioZH.big";

/// <summary>
/// Wildcard file pattern for any Zero Hour BIG files: "*ZH.big".
/// </summary>
public const string AnyZeroHourBig = "*ZH.big";
}
159 changes: 92 additions & 67 deletions GenHub/GenHub.Core/Features/GameInstallations/LanguageDetector.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using GenHub.Core.Constants;
using GenHub.Core.Models.Content;
using System;
using System.IO;
using System.Linq;
Expand All @@ -19,125 +21,148 @@ public class LanguageDetector : ILanguageDetector
/// <returns>The detected language code in uppercase (e.g., "EN", "DE"), or "EN" as fallback.</returns>
public Task<string> DetectAsync(string installationPath, CancellationToken cancellationToken = default)
{
if (!Directory.Exists(installationPath))
cancellationToken.ThrowIfCancellationRequested();

if (string.IsNullOrWhiteSpace(installationPath) || !Directory.Exists(installationPath))
{
return Task.FromResult("EN"); // Fallback
return Task.FromResult(CsvConstants.LanguageEn);
}

// Check for language-specific directories and files
var languageMappings = new[]
// Check for language-specific directories
var directoryMappings = new (string RelativeDir, string Language)[]
{
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" },
(LanguageDirectoryNames.DataEnglish, CsvConstants.LanguageEn),
(LanguageDirectoryNames.DataEnglishUppercase, CsvConstants.LanguageEn),
(LanguageDirectoryNames.DataGerman, CsvConstants.LanguageDe),
(LanguageDirectoryNames.DataDeutsch, CsvConstants.LanguageDe),
(LanguageDirectoryNames.DataFrench, CsvConstants.LanguageFr),
(LanguageDirectoryNames.DataSpanish, CsvConstants.LanguageEs),
(LanguageDirectoryNames.DataItalian, CsvConstants.LanguageIt),
(LanguageDirectoryNames.DataKorean, CsvConstants.LanguageKo),
(LanguageDirectoryNames.DataPolish, CsvConstants.LanguagePl),
(LanguageDirectoryNames.DataPortuguese, CsvConstants.LanguagePtBr),
(LanguageDirectoryNames.DataChinese, CsvConstants.LanguageZhCn),
(LanguageDirectoryNames.DataChineseTraditional, CsvConstants.LanguageZhTw),
};

foreach (var mapping in languageMappings)
foreach (var (relativeDir, language) in directoryMappings)
{
if (Directory.Exists(Path.Combine(installationPath, mapping.Pattern)))
var dirPath = CombineRelativePath(installationPath, relativeDir);
if (Directory.Exists(dirPath))
{
return Task.FromResult(mapping.Language);
return Task.FromResult(ContentSearchQuery.NormalizeLanguage(language));
}
}

// Check for language-specific files
var fileMappings = new[]
var fileMappings = new (string FileName, string Language)[]
{
// English
new { Pattern = "English.big", Language = "EN" },
new { Pattern = "AudioEnglish.big", Language = "EN" },
new { Pattern = "SpeechEnglish.big", Language = "EN" },
(LanguageFilePatterns.EnglishBig, CsvConstants.LanguageEn),
(LanguageFilePatterns.AudioEnglishBig, CsvConstants.LanguageEn),
(LanguageFilePatterns.SpeechEnglishBig, CsvConstants.LanguageEn),

// German
new { Pattern = "German.big", Language = "DE" },
new { Pattern = "AudioGerman.big", Language = "DE" },
(LanguageFilePatterns.GermanBig, CsvConstants.LanguageDe),
(LanguageFilePatterns.AudioGermanBig, CsvConstants.LanguageDe),

// French
new { Pattern = "French.big", Language = "FR" },
new { Pattern = "AudioFrench.big", Language = "FR" },
(LanguageFilePatterns.FrenchBig, CsvConstants.LanguageFr),
(LanguageFilePatterns.AudioFrenchBig, CsvConstants.LanguageFr),

// Spanish
new { Pattern = "Spanish.big", Language = "ES" },
new { Pattern = "AudioSpanish.big", Language = "ES" },
(LanguageFilePatterns.SpanishBig, CsvConstants.LanguageEs),
(LanguageFilePatterns.AudioSpanishBig, CsvConstants.LanguageEs),

// Italian
new { Pattern = "Italian.big", Language = "IT" },
new { Pattern = "AudioItalian.big", Language = "IT" },
(LanguageFilePatterns.ItalianBig, CsvConstants.LanguageIt),
(LanguageFilePatterns.AudioItalianBig, CsvConstants.LanguageIt),

// Korean
new { Pattern = "Korean.big", Language = "KO" },
new { Pattern = "AudioKorean.big", Language = "KO" },
(LanguageFilePatterns.KoreanBig, CsvConstants.LanguageKo),
(LanguageFilePatterns.AudioKoreanBig, CsvConstants.LanguageKo),

// Polish
new { Pattern = "Polish.big", Language = "PL" },
new { Pattern = "AudioPolish.big", Language = "PL" },
(LanguageFilePatterns.PolishBig, CsvConstants.LanguagePl),
(LanguageFilePatterns.AudioPolishBig, CsvConstants.LanguagePl),

// Portuguese-Brazil
new { Pattern = "PortugueseBrazil.big", Language = "PT-BR" },
new { Pattern = "AudioPortugueseBrazil.big", Language = "PT-BR" },
(LanguageFilePatterns.PortugueseBrazilBig, CsvConstants.LanguagePtBr),
(LanguageFilePatterns.AudioPortugueseBrazilBig, CsvConstants.LanguagePtBr),

// Chinese Simplified
new { Pattern = "Chinese.big", Language = "ZH-CN" },
new { Pattern = "AudioChinese.big", Language = "ZH-CN" },
(LanguageFilePatterns.ChineseBig, CsvConstants.LanguageZhCn),
(LanguageFilePatterns.AudioChineseBig, CsvConstants.LanguageZhCn),

// Chinese Traditional
new { Pattern = "ChineseTraditional.big", Language = "ZH-TW" },
new { Pattern = "AudioChineseTraditional.big", Language = "ZH-TW" },
(LanguageFilePatterns.ChineseTraditionalBig, CsvConstants.LanguageZhTw),
(LanguageFilePatterns.AudioChineseTraditionalBig, CsvConstants.LanguageZhTw),
};

foreach (var mapping in fileMappings)
foreach (var (fileName, language) in fileMappings)
{
if (File.Exists(Path.Combine(installationPath, mapping.Pattern)))
var filePath = Path.Combine(installationPath, fileName);
if (File.Exists(filePath))
{
return Task.FromResult(mapping.Language);
return Task.FromResult(ContentSearchQuery.NormalizeLanguage(language));
}
}

// Check for Zero Hour specific patterns
var zhPatterns = new[]
var zhPatterns = new (string Pattern, string Language)[]
{
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" },
(LanguageFilePatterns.EnglishZHBig, CsvConstants.LanguageEn),
(LanguageFilePatterns.AudioZHBig, CsvConstants.LanguageEn),
(GameClientConstants.ZeroHourIniBig, CsvConstants.LanguageEn),
(LanguageFilePatterns.GermanZHBig, CsvConstants.LanguageDe),
(LanguageFilePatterns.FrenchZHBig, CsvConstants.LanguageFr),
(LanguageFilePatterns.SpanishZHBig, CsvConstants.LanguageEs),
(LanguageFilePatterns.ItalianZHBig, CsvConstants.LanguageIt),
(LanguageFilePatterns.KoreanZHBig, CsvConstants.LanguageKo),
(LanguageFilePatterns.PolishZHBig, CsvConstants.LanguagePl),
(LanguageFilePatterns.PortugueseZHBig, CsvConstants.LanguagePtBr),
(LanguageFilePatterns.ChineseZHBig, CsvConstants.LanguageZhCn),
(LanguageFilePatterns.AnyZeroHourBig, CsvConstants.LanguageEn),
};

foreach (var mapping in zhPatterns)
foreach (var (pattern, language) in zhPatterns)
{
if (mapping.Pattern.Contains("*"))
if (pattern.Contains('*'))
{
// Handle wildcard
var files = Directory.GetFiles(installationPath, mapping.Pattern, SearchOption.AllDirectories);
if (files.Length > 0)
try
{
var files = Directory.GetFiles(installationPath, pattern, SearchOption.AllDirectories);
if (files.Length > 0)
{
return Task.FromResult(ContentSearchQuery.NormalizeLanguage(language));
}
}
catch (IOException)
{
// Fall through on IO issues
}
catch (UnauthorizedAccessException)
{
return Task.FromResult(mapping.Language);
// Fall through on permission issues
}
}
else if (File.Exists(Path.Combine(installationPath, mapping.Pattern)))
else
{
return Task.FromResult(mapping.Language);
var filePath = Path.Combine(installationPath, pattern);
if (File.Exists(filePath))
{
return Task.FromResult(ContentSearchQuery.NormalizeLanguage(language));
}
}
}

// Fallback to English
return Task.FromResult("EN");
return Task.FromResult(CsvConstants.LanguageEn);
}

private static string CombineRelativePath(string basePath, string relativePath)
{
Comment thread
undead2146 marked this conversation as resolved.
var segments = relativePath.Split(['/', '\\'], StringSplitOptions.RemoveEmptyEntries);
return Path.Combine(segments.Prepend(basePath).ToArray());
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using GenHub.Core.Models.Enums;
using GenHub.Core.Models.GameInstallations;
using GenHub.Core.Models.Results;
using GenHub.Core.Models.Validation;
Expand All @@ -24,5 +28,31 @@ public interface IGameInstallationValidator
/// <param name="progress">Progress reporter for MVVM integration.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>A <see cref="ValidationResult"/> representing the outcome of the validation.</returns>
Task<ValidationResult> ValidateAsync(GameInstallation installation, IProgress<ValidationProgress>? progress = null, CancellationToken cancellationToken = default);
Task<ValidationResult> ValidateAsync(GameInstallation installation, IProgress<ValidationProgress>? progress, CancellationToken cancellationToken = default);

/// <summary>
/// Validates a game installation with an explicit language and optional progress reporting.
/// </summary>
/// <param name="installation">The game installation to validate.</param>
/// <param name="language">The explicit language code (e.g., "EN", "DE").</param>
/// <param name="progress">Progress reporter for MVVM integration.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>A <see cref="ValidationResult"/> representing the outcome of the validation.</returns>
Task<ValidationResult> ValidateAsync(GameInstallation installation, string language, IProgress<ValidationProgress>? progress = null, CancellationToken cancellationToken = default);

/// <summary>
/// Validates a specific game installation directory by path, game type, and optional language.
/// </summary>
/// <param name="installationPath">The path to the game directory.</param>
/// <param name="gameType">The target game type (Generals or ZeroHour).</param>
/// <param name="language">Optional explicit language code. If null, language is auto-detected.</param>
/// <param name="progress">Progress reporter for MVVM integration.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>A <see cref="ValidationResult"/> representing the outcome of the validation.</returns>
Task<ValidationResult> ValidateInstallationAsync(
string installationPath,
GameType gameType,
string? language = null,
IProgress<ValidationProgress>? progress = null,
CancellationToken cancellationToken = default);
}
14 changes: 13 additions & 1 deletion GenHub/GenHub.Core/Models/Results/Validation/ValidationResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace GenHub.Core.Models.Results;
using GenHub.Core.Models.Validation;

/// <summary>Encapsulates the result of a validation operation for a game version or installation.</summary>
public class ValidationResult(string validatedTargetId, List<ValidationIssue>? issues, TimeSpan elapsed = default)
public class ValidationResult(string validatedTargetId, List<ValidationIssue>? issues, TimeSpan elapsed = default, int totalFilesValidated = 0)
: ResultBase(DetermineSuccess(issues), ExtractErrorMessages(issues), elapsed)
{
/// <summary>Gets the unique ID of the target that was validated (e.g., a GameClient ID or a GameInstallation ID).</summary>
Expand All @@ -17,6 +17,18 @@ public class ValidationResult(string validatedTargetId, List<ValidationIssue>? i
/// <summary>Gets a value indicating whether the target is considered valid.</summary>
public bool IsValid => Success;

/// <summary>Gets the total number of files validated.</summary>
public int TotalFilesValidated { get; init; } = totalFilesValidated;

/// <summary>Gets the count of missing files.</summary>
public int MissingFilesCount => Issues.Count(i => i.IssueType == ValidationIssueType.MissingFile);

/// <summary>Gets the count of corrupted or size-mismatched files.</summary>
public int CorruptedFilesCount => Issues.Count(i => i.IssueType == ValidationIssueType.CorruptedFile || i.IssueType == ValidationIssueType.MismatchedFileSize);

/// <summary>Gets the count of extra or unexpected files.</summary>
public int ExtraFilesCount => Issues.Count(i => i.IssueType == ValidationIssueType.UnexpectedFile);

/// <summary>Gets the count of critical issues that prevent the target from being considered valid.</summary>
public int CriticalIssueCount => Issues.Count(i => i.Severity == ValidationSeverity.Error || i.Severity == ValidationSeverity.Critical);

Expand Down
Loading
Loading