Skip to content

feat(installations): detect retail installations by archive presence - #352

Open
bobtista wants to merge 3 commits into
developmentfrom
feat/archive-presence-detection
Open

feat(installations): detect retail installations by archive presence#352
bobtista wants to merge 3 commits into
developmentfrom
feat/archive-presence-detection

Conversation

@bobtista

@bobtista bobtista commented Aug 3, 2026

Copy link
Copy Markdown

Summary

Detects retail installations by the presence of retail archives rather than by
executable name, and scans launch candidates through the executable classifier.

Closes #330.

Why

Executable-name detection misses a retail install whose engine binary is
extensionless — the shape a native Mach-O or ELF build has — and misses combined
directories where retail archives sit beside publisher binaries. Archive presence
is the property that actually identifies a retail install.

Rebase note

This branch was stacked on feat/native-launch, which squash-merged as #332. Its
own commits were replayed onto development with
git rebase --onto origin/development d1adeeb.

The replay applied cleanly but did not compile, which is worth recording: #339
split ExecutableFileClassifier.IsLegacyLaunchCandidate into a name-only overload
and a content-aware (path, absolutePath) overload, and this branch's two call
sites passed the old single-argument form as a method group.

Both sites enumerate real files from Directory.EnumerateFiles, so per the
classifier's own guidance — "whenever the file is on disk, call
IsLegacyLaunchCandidate(path, absolutePath) so extensionless files are judged by
their magic bytes"
— they now use the content-aware overload.

That surfaced a second failure. GameClientDetectorTests wrote "dummy content" as
its extensionless native-binary fixture, which magic-byte classification correctly
rejects. The fixture now writes real ELF magic
(0x7F 0x45 0x4C 0x46 0x02 0x01 0x01 0x00), matching the fixtures #339 introduced.

The GetLaunchCandidateFiles remark claimed content-based classification would slot
in "without this site changing". It did change, so the remark was corrected.

These adaptations are isolated in their own commit rather than folded into the
feature commits, so the API drift is reviewable on its own.

Testing

  • dotnet test GenHub/GenHub.Tests/GenHub.Tests.Core/GenHub.Tests.Core.csproj -c Release
    • Passed: 1,556
    • Failed: 0

Greptile Summary

This PR replaces executable-name-based retail installation detection with archive classification, adds combined-installation manifest and validation handling, and expands executable scanning to native binaries.

  • Adds shared retail archive classification and uses it in installation detection and path validation.
  • Supports per-game manifest retrieval and validation for combined Generals/Zero Hour directories.
  • Scans launch candidates through content-aware executable classification on Windows and macOS.
  • The broad Zero Hour archive predicate can misclassify unrelated directories, while combined paths lose their standard Generals client.

Confidence Score: 4/5

The PR should not merge until false retail-install detection and the missing standard Generals client for combined installations are fixed.

A single non-retail *ZH.big file in a broad macOS search root can create a bogus installation, and legitimate combined installations no longer produce the standard Generals client required by profile and manifest consumers.

Files Needing Attention: GenHub/GenHub.Core/Helpers/RetailArchiveClassifier.cs, GenHub/GenHub.MacOS/GameInstallations/MacOSInstallationDetector.cs, GenHub/GenHub/Features/GameClients/GameClientDetector.cs

Important Files Changed

Filename Overview
GenHub/GenHub.Core/Helpers/RetailArchiveClassifier.cs Introduces centralized archive detection, but the unrestricted zh.big suffix accepts known non-retail community archives as installation evidence.
GenHub/GenHub.Core/Models/GameInstallations/GameInstallation.cs Replaces executable checks with archive classification and supports shared paths for combined installations.
GenHub/GenHub.MacOS/GameInstallations/MacOSInstallationDetector.cs Detects flat native installations, but directly classifying broad roots such as Downloads exposes false retail installations.
GenHub/GenHub.Windows/GameInstallations/WindowsInstallationDetector.cs Adds archive-based flat-install detection and preserves combined directories as one deduplication unit.
GenHub/GenHub/Features/GameClients/GameClientDetector.cs Adds native launch-candidate scanning but suppresses the standard Generals client whenever both games share a directory.
GenHub/GenHub/Features/Manifest/ManifestProvider.cs Adds explicit per-game manifest lookup while preserving the Zero Hour preference of the legacy overload.
GenHub/GenHub/Features/Validation/GameInstallationValidator.cs Validates combined installations once per game and filters sibling root archives from extraneous-file warnings.
GenHub/GenHub/Features/Content/Services/Publishers/SuperHackersManifestFactory.cs Extends executable discovery to content-classified extensionless native binaries.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  Root[Candidate directory] --> Archive[Classify retail archives]
  Archive --> Install[Set game flags and paths]
  Install --> Clients[Detect standard and publisher clients]
  Install --> Manifests[Resolve per-game manifests]
  Manifests --> Validation[Validate each detected game]
  Clients --> Profiles[Create game profiles]
Loading
Prompt To Fix All With AI
### Issue 1
GenHub/GenHub.Core/Helpers/RetailArchiveClassifier.cs:57-60
**Non-retail archives trigger installation detection**

When a broad macOS search root such as Downloads contains a community archive like `340_ControlBarProZH.big`, this suffix check marks the entire directory as a Zero Hour retail installation, causing unrelated files to be cached and processed as game content.

### Issue 2
GenHub/GenHub/Features/GameClients/GameClientDetector.cs:72-78
**Combined installs lose Generals client**

When both games share one directory, this branch skips creation of the standard Generals client while still creating the Zero Hour client, causing automatic Generals profile creation and Generals manifest or dependency resolution to fail despite Generals being detected.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix(detection): classify launch candidat..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

Context used (3)

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Improved detection of Generals and Zero Hour installations using recognized game archives.
    • Added support for combined installations containing both games in one directory.
    • Added support for native, extensionless game executables, including on macOS.
    • Manifest selection now supports explicitly choosing Generals or Zero Hour.
  • Bug Fixes

    • Improved validation of flat, nested, Zero Hour-only, and combined installations.
    • Reduced false positives for mod-only archives and unrelated files.
    • Improved handling of duplicate installation paths and inaccessible directories.
  • Tests

    • Added comprehensive coverage for archive detection, combined installations, native executables, and validation scenarios.

Walkthrough

Retail installation detection now uses canonical archive presence instead of executable names. Combined Generals and Zero Hour directories receive typed manifests and validation. Windows and macOS detection support flat roots and native deployment paths. Client discovery also recognizes extensionless native binaries.

Changes

Retail detection and validation

Layer / File(s) Summary
Archive classification contract
GenHub/GenHub.Core/Constants/*, GenHub/GenHub.Core/Helpers/RetailArchiveClassifier.cs, GenHub/GenHub.Core/Models/GameInstallations/RetailArchiveClassification.cs, GenHub/GenHub.Tests/GenHub.Tests.Core/Helpers/*
Canonical Generals archives and Zero Hour suffixes now produce a two-flag classification for root-level retail archives.
Cross-platform installation detection
GenHub/GenHub.Core/Models/GameInstallations/*, GenHub/GenHub.Windows/GameInstallations/*, GenHub/GenHub.MacOS/GameInstallations/*, related tests
Installation discovery uses archive classification, supports combined directories, handles filesystem failures, and searches the native macOS deployment directory.
Typed manifests and combined validation
GenHub/GenHub.Core/Interfaces/Manifest/*, GenHub/GenHub/Features/Manifest/*, GenHub/GenHub/Features/Validation/*, GenHub/GenHub/Features/Content/Services/ContentValidator.cs, related tests
Manifest retrieval accepts an explicit game type. Validation processes Generals and Zero Hour separately and excludes sibling retail archives from unexpected-file issues.
Native client detection
GenHub/GenHub/Features/GameClients/*, GenHub/GenHub/Features/Content/Services/Publishers/*, GenHub/GenHub.Core/Constants/GameClientConstants.cs, related tests
Client scans recognize extensionless native launch candidates and avoid duplicate standard-client scans for combined paths.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MacOSInstallationDetector
  participant RetailArchiveClassifier
  participant GameInstallation
  MacOSInstallationDetector->>RetailArchiveClassifier: Classify candidate root archives
  RetailArchiveClassifier-->>MacOSInstallationDetector: Return archive flags
  MacOSInstallationDetector->>GameInstallation: Create installation from detected paths
  GameInstallation->>RetailArchiveClassifier: Classify each game path
  RetailArchiveClassifier-->>GameInstallation: Return Generals and Zero Hour flags
Loading

Possibly related PRs

Suggested labels: Enhancement, Testing

Poem

A rabbit checks the archives bright,
INI.big and INIZH.big in sight.
Two game paths share one home,
Native binaries freely roam.
Manifests bloom, tests softly sing—
Hop, hop, a combined installation wins!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning GameClientDetector and SuperHackersManifestFactory changes extend extensionless-binary launch detection, which issue #330 explicitly excludes for #325. Move the launch-candidate and extensionless-binary changes to #325, or link the issue that owns this scope.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title uses conventional commit syntax and clearly describes the archive-based retail installation detection change.
Description check ✅ Passed The description directly explains archive-based detection, extensionless binaries, testing, and the related issue.
Linked Issues check ✅ Passed The archive classifier, root-only detection, combined-installation handling, game-specific manifests, and native macOS path address issue #330.
Docstring Coverage ✅ Passed Docstring coverage is 96.55% which is sufficient. The required threshold is 50.00%.
📋 Issue Planner

Built with CodeRabbit's Coding Plans for faster development and fewer bugs.

View plan used: #330

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/archive-presence-detection

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added Enhancement New feature or request Testing Topic related to (unit) tests labels Aug 3, 2026
Comment on lines +57 to +60
if (!hasZeroHour &&
archiveName.EndsWith(RetailArchiveConstants.ZeroHourArchiveSuffix, StringComparison.OrdinalIgnoreCase))
{
hasZeroHour = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Non-retail archives trigger installation detection

When a broad macOS search root such as Downloads contains a community archive like 340_ControlBarProZH.big, this suffix check marks the entire directory as a Zero Hour retail installation, causing unrelated files to be cached and processed as game content.

Knowledge Base Used: Game Detection: Installations and Client Identification

Prompt To Fix With AI
This is a comment left during a code review.
Path: GenHub/GenHub.Core/Helpers/RetailArchiveClassifier.cs
Line: 57-60

Comment:
**Non-retail archives trigger installation detection**

When a broad macOS search root such as Downloads contains a community archive like `340_ControlBarProZH.big`, this suffix check marks the entire directory as a Zero Hour retail installation, causing unrelated files to be cached and processed as game content.

**Knowledge Base Used:** [Game Detection: Installations and Client Identification](https://app.greptile.com/genhub/-/custom-context/knowledge-base/community-outpost/genhub/-/docs/game-detection.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +72 to 78
if (isCombinedDirectory)
{
var generalsVersion = new GameClient
{
Name = $"Generals {version}",
Id = string.Empty, // Set later by manifest
Version = version,
ExecutablePath = actualExePath,
GameType = GameType.Generals,
InstallationId = inst.Id,
WorkingDirectory = inst.GeneralsPath,
};
await GenerateClientManifestAndSetIdAsync(generalsVersion, inst.GeneralsPath, inst, GameType.Generals);
gameClients.Add(generalsVersion);
logger.LogDebug(
"Skipping standard Generals client scan for {InstallationId}: {GeneralsPath} is a combined directory scanned once as Zero Hour",
inst.Id,
inst.GeneralsPath);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Combined installs lose Generals client

When both games share one directory, this branch skips creation of the standard Generals client while still creating the Zero Hour client, causing automatic Generals profile creation and Generals manifest or dependency resolution to fail despite Generals being detected.

Knowledge Base Used: Game Detection: Installations and Client Identification

Prompt To Fix With AI
This is a comment left during a code review.
Path: GenHub/GenHub/Features/GameClients/GameClientDetector.cs
Line: 72-78

Comment:
**Combined installs lose Generals client**

When both games share one directory, this branch skips creation of the standard Generals client while still creating the Zero Hour client, causing automatic Generals profile creation and Generals manifest or dependency resolution to fail despite Generals being detected.

**Knowledge Base Used:** [Game Detection: Installations and Client Identification](https://app.greptile.com/genhub/-/custom-context/knowledge-base/community-outpost/genhub/-/docs/game-detection.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@GenHub/GenHub.Core/Models/GameInstallations/GameInstallation.cs`:
- Around line 248-269: The archive classification safety logic is duplicated
across two callers. In
GenHub/GenHub.Core/Models/GameInstallations/GameInstallation.cs#L248-L269,
replace ClassifyArchivesSafely with a shared
RetailArchiveClassifier.TryClassifyArchives(string path, out
RetailArchiveClassification classification) call while preserving SetPaths’s
existing warning log; in
GenHub/GenHub.Windows/GameInstallations/WindowsInstallationDetector.cs#L314-L331,
remove the duplicate helper and call the shared method from
DetectRetailInstallations, allowing each caller to retain its own warning
behavior.

In `@GenHub/GenHub.Windows/GameInstallations/WindowsInstallationDetector.cs`:
- Around line 222-243: Update the IsCombinedDirectory branch to skip the
installation only when combinedPath exists in both seenGeneralsPaths and
seenZeroHourPaths; retain it when only one game has already been claimed,
matching the non-combined deduplication behavior.

In `@GenHub/GenHub/Features/GameInstallations/InstallationPathResolver.cs`:
- Around line 97-111: Update IsValidGameInstallationAsync, which filters
candidates for SearchForInstallationAsync, to validate Generals and Zero Hour
directories using RetailArchiveClassifier.ClassifyArchives and the corresponding
archive flags, matching the archive-based validation in
ResolveInstallationPathAsync. Remove the requirement that generals.exe must
exist so moved native installations can be recovered.

In `@GenHub/GenHub/Features/Validation/GameInstallationValidator.cs`:
- Around line 82-88: The combined-directory predicate is duplicated across two
files; extract it into one shared helper on GameInstallation or as an extension,
preserving both-game flags, non-empty paths, normalized full-path comparison,
and case-insensitive matching. Update GameInstallationValidator.cs lines 82-88
and GameClientDetector.cs lines 64-68 to call the shared helper instead of
computing the predicate inline.
- Line 119: Update the nested content validation call in the outer validation
loop to prevent ContentValidator.ValidateAllAsync from reporting its own Total=3
values through the outer progress subscriber. Pass null for progress, or use an
adapter that rescales nested ValidationProgress reports to the outer totalSteps
scale while preserving the outer progress contract.
- Around line 108-124: Remove the separate
contentValidator.ValidateManifestAsync call and its
manifestValidationResult.Issues addition from the validation flow. Keep
ValidateAllAsync as the single manifest and content validation entry point,
preserving the existing progress reporting and content-issue filtering.
- Around line 140-143: Update the required-directory validation in the manifest
validation flow so ValidateDirectoriesAsync runs whenever requiredDirs contains
entries, including the fallback case where gameType is null. Preserve the
existing requiredDirs guard and ensure fallback manifests still report missing
Data or Maps directories.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9b2c81ba-210f-469b-aaa6-e6f188e7a753

📥 Commits

Reviewing files that changed from the base of the PR and between b3f5c4a and 31f0b24.

📒 Files selected for processing (20)
  • GenHub/GenHub.Core/Constants/GameClientConstants.cs
  • GenHub/GenHub.Core/Constants/RetailArchiveConstants.cs
  • GenHub/GenHub.Core/Helpers/RetailArchiveClassifier.cs
  • GenHub/GenHub.Core/Interfaces/Manifest/IManifestProvider.cs
  • GenHub/GenHub.Core/Models/GameInstallations/GameInstallation.cs
  • GenHub/GenHub.Core/Models/GameInstallations/RetailArchiveClassification.cs
  • GenHub/GenHub.MacOS/GameInstallations/MacOSInstallationDetector.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameClients/GameClientDetectorTests.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Manifest/ManifestProviderTests.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Validation/GameInstallationValidatorTests.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Helpers/RetailArchiveClassifierTests.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Models/GameInstallations/GameInstallationTests.cs
  • GenHub/GenHub.Tests/GenHub.Tests.MacOS/GameInstallations/MacOSInstallationDetectorTests.cs
  • GenHub/GenHub.Windows/GameInstallations/WindowsInstallationDetector.cs
  • GenHub/GenHub/Features/Content/Services/ContentValidator.cs
  • GenHub/GenHub/Features/Content/Services/Publishers/SuperHackersManifestFactory.cs
  • GenHub/GenHub/Features/GameClients/GameClientDetector.cs
  • GenHub/GenHub/Features/GameInstallations/InstallationPathResolver.cs
  • GenHub/GenHub/Features/Manifest/ManifestProvider.cs
  • GenHub/GenHub/Features/Validation/GameInstallationValidator.cs

Comment on lines +248 to 269
/// <summary>
/// Classifies a directory's archives without letting a filesystem error escape.
/// </summary>
/// <param name="path">The directory to classify.</param>
/// <returns>The classification, or neither game when the directory cannot be read.</returns>
/// <remarks>
/// <see cref="SetPaths"/> is called from every platform detector, so it must not
/// throw. An unreadable directory is logged rather than silently reading as "no
/// archives" — the flag still ends up false, but the log names the real cause.
/// </remarks>
private RetailArchiveClassification ClassifyArchivesSafely(string path)
{
var possibleExes = new[] { GameClientConstants.SteamGameDatExecutable, GameClientConstants.GeneralsExecutable, GameClientConstants.ZeroHourExecutable };
return possibleExes.Any(exe => Path.Combine(path, exe).FileExistsCaseInsensitive());
try
{
return RetailArchiveClassifier.ClassifyArchives(path);
}
catch (Exception ex) when (ex is UnauthorizedAccessException or IOException)
{
_logger?.LogWarning(ex, "Could not read {Path} while classifying retail archives; treating it as holding none", path);
return default;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated ClassifyArchivesSafely in two files. Both files define the same private helper with the same UnauthorizedAccessException or IOException filter, because RetailArchiveClassifier exposes no safe entry point. Add one, for example RetailArchiveClassifier.TryClassifyArchives(string path, out RetailArchiveClassification classification), and let each caller log its own warning.

  • GenHub/GenHub.Core/Models/GameInstallations/GameInstallation.cs#L248-L269: replace the private helper with the shared classifier method and keep the existing warning log in SetPaths.
  • GenHub/GenHub.Windows/GameInstallations/WindowsInstallationDetector.cs#L314-L331: delete the private helper and call the shared classifier method from DetectRetailInstallations.
📍 Affects 2 files
  • GenHub/GenHub.Core/Models/GameInstallations/GameInstallation.cs#L248-L269 (this comment)
  • GenHub/GenHub.Windows/GameInstallations/WindowsInstallationDetector.cs#L314-L331
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@GenHub/GenHub.Core/Models/GameInstallations/GameInstallation.cs` around lines
248 - 269, The archive classification safety logic is duplicated across two
callers. In
GenHub/GenHub.Core/Models/GameInstallations/GameInstallation.cs#L248-L269,
replace ClassifyArchivesSafely with a shared
RetailArchiveClassifier.TryClassifyArchives(string path, out
RetailArchiveClassification classification) call while preserving SetPaths’s
existing warning log; in
GenHub/GenHub.Windows/GameInstallations/WindowsInstallationDetector.cs#L314-L331,
remove the duplicate helper and call the shared method from
DetectRetailInstallations, allowing each caller to retain its own warning
behavior.

Comment on lines +222 to +243
// A combined directory — both games flagged at the same path — is one unit.
// Splitting it across sources by clearing whichever game another source
// already claimed would leave the same directory owned by two installations
// and scanned twice for clients, so it is kept whole or dropped whole.
if (IsCombinedDirectory(installation))
{
var combinedPath = Path.GetFullPath(installation.GeneralsPath);
if (seenGeneralsPaths.Contains(combinedPath) || seenZeroHourPaths.Contains(combinedPath))
{
logger.LogWarning(
"Skipping combined {InstallationType} installation at {CombinedPath} (directory already detected from another source)",
installation.InstallationType,
combinedPath);
continue;
}

seenGeneralsPaths.Add(combinedPath);
seenZeroHourPaths.Add(combinedPath);
deduplicated.Add(installation);
continue;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

A partially claimed combined directory loses one game.

The branch drops the combined installation when either seen-path set already contains the directory. If a higher-priority source claimed only Generals at that directory, Zero Hour is then reported by no installation. The non-combined branch below keeps the unique game in the same situation, so the two branches disagree.

Skip only when both games are already claimed.

🐛 Proposed fix
             if (IsCombinedDirectory(installation))
             {
                 var combinedPath = Path.GetFullPath(installation.GeneralsPath);
-                if (seenGeneralsPaths.Contains(combinedPath) || seenZeroHourPaths.Contains(combinedPath))
+                if (seenGeneralsPaths.Contains(combinedPath) && seenZeroHourPaths.Contains(combinedPath))
                 {
                     logger.LogWarning(
                         "Skipping combined {InstallationType} installation at {CombinedPath} (directory already detected from another source)",
                         installation.InstallationType,
                         combinedPath);
                     continue;
                 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// A combined directory — both games flagged at the same path — is one unit.
// Splitting it across sources by clearing whichever game another source
// already claimed would leave the same directory owned by two installations
// and scanned twice for clients, so it is kept whole or dropped whole.
if (IsCombinedDirectory(installation))
{
var combinedPath = Path.GetFullPath(installation.GeneralsPath);
if (seenGeneralsPaths.Contains(combinedPath) || seenZeroHourPaths.Contains(combinedPath))
{
logger.LogWarning(
"Skipping combined {InstallationType} installation at {CombinedPath} (directory already detected from another source)",
installation.InstallationType,
combinedPath);
continue;
}
seenGeneralsPaths.Add(combinedPath);
seenZeroHourPaths.Add(combinedPath);
deduplicated.Add(installation);
continue;
}
// A combined directory — both games flagged at the same path — is one unit.
// Splitting it across sources by clearing whichever game another source
// already claimed would leave the same directory owned by two installations
// and scanned twice for clients, so it is kept whole or dropped whole.
if (IsCombinedDirectory(installation))
{
var combinedPath = Path.GetFullPath(installation.GeneralsPath);
if (seenGeneralsPaths.Contains(combinedPath) && seenZeroHourPaths.Contains(combinedPath))
{
logger.LogWarning(
"Skipping combined {InstallationType} installation at {CombinedPath} (directory already detected from another source)",
installation.InstallationType,
combinedPath);
continue;
}
seenGeneralsPaths.Add(combinedPath);
seenZeroHourPaths.Add(combinedPath);
deduplicated.Add(installation);
continue;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@GenHub/GenHub.Windows/GameInstallations/WindowsInstallationDetector.cs`
around lines 222 - 243, Update the IsCombinedDirectory branch to skip the
installation only when combinedPath exists in both seenGeneralsPaths and
seenZeroHourPaths; retain it when only one game has already been claimed,
matching the non-combined deduplication behavior.

Comment on lines +97 to +111
// Check if it still holds the games' retail archives — the same signal that
// flagged the games at detection time, so a native install whose binary is
// extensionless does not read as vanished.
var hasValidFiles = false;

if (installation.HasGenerals && !string.IsNullOrEmpty(installation.GeneralsPath))
if (installation.HasGenerals && !string.IsNullOrEmpty(installation.GeneralsPath)
&& RetailArchiveClassifier.ClassifyArchives(installation.GeneralsPath).HasGeneralsArchives)
{
var generalsExe = Path.Combine(installation.GeneralsPath, "generals.exe");
if (File.Exists(generalsExe))
{
hasValidFiles = true;
}
hasValidFiles = true;
}

if (installation.HasZeroHour && !string.IsNullOrEmpty(installation.ZeroHourPath))
if (installation.HasZeroHour && !string.IsNullOrEmpty(installation.ZeroHourPath)
&& RetailArchiveClassifier.ClassifyArchives(installation.ZeroHourPath).HasZeroHourArchives)
{
var zhExe = Path.Combine(installation.ZeroHourPath, "generals.exe");
if (File.Exists(zhExe))
{
hasValidFiles = true;
}
hasValidFiles = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Path recovery still requires generals.exe, so native installs cannot be re-resolved.

Validation now accepts a directory based on retail archives. When validation fails, ResolveInstallationPathAsync calls SearchForInstallationAsync, and its candidate filter IsValidGameInstallationAsync (Lines 295-300) returns false unless generals.exe exists. A moved native installation therefore validates as invalid and is never recovered, which is the case this PR targets.

Apply the same archive classification in the candidate filter.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@GenHub/GenHub/Features/GameInstallations/InstallationPathResolver.cs` around
lines 97 - 111, Update IsValidGameInstallationAsync, which filters candidates
for SearchForInstallationAsync, to validate Generals and Zero Hour directories
using RetailArchiveClassifier.ClassifyArchives and the corresponding archive
flags, matching the archive-based validation in ResolveInstallationPathAsync.
Remove the requirement that generals.exe must exist so moved native
installations can be recovered.

Comment on lines +82 to +88
var isCombinedDirectory = installation.HasGenerals
&& installation.HasZeroHour
&& !string.IsNullOrEmpty(installation.GeneralsPath)
&& !string.IsNullOrEmpty(installation.ZeroHourPath)
&& Path.GetFullPath(installation.GeneralsPath).Equals(
Path.GetFullPath(installation.ZeroHourPath),
StringComparison.OrdinalIgnoreCase);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Combined-directory detection is duplicated identically across two files. Both sites compute the same "both games flagged at the same normalized path" predicate independently. This is one predicate, not two; if it changes in one file without the other, validation and client detection can disagree about which directories are combined.

  • GenHub/GenHub/Features/Validation/GameInstallationValidator.cs#L82-L88: extract this computation into a shared helper (for example an IsCombinedRetailDirectory extension or method on GameInstallation) and call it here.
  • GenHub/GenHub/Features/GameClients/GameClientDetector.cs#L64-L68: call the same shared helper here instead of recomputing the predicate inline.
📍 Affects 2 files
  • GenHub/GenHub/Features/Validation/GameInstallationValidator.cs#L82-L88 (this comment)
  • GenHub/GenHub/Features/GameClients/GameClientDetector.cs#L64-L68
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@GenHub/GenHub/Features/Validation/GameInstallationValidator.cs` around lines
82 - 88, The combined-directory predicate is duplicated across two files;
extract it into one shared helper on GameInstallation or as an extension,
preserving both-game flags, non-empty paths, normalized full-path comparison,
and case-insensitive matching. Update GameInstallationValidator.cs lines 82-88
and GameClientDetector.cs lines 64-68 to call the shared helper instead of
computing the predicate inline.

Comment on lines 108 to +124

// Installation-specific validations (directories, etc.)
var requiredDirs = manifest.RequiredDirectories ?? Enumerable.Empty<string>();
if (requiredDirs.Any())
{
if (installation.HasGenerals)
progress?.Report(new ValidationProgress(++currentStep, totalSteps, "Core manifest validation"));

var manifestValidationResult = await contentValidator.ValidateManifestAsync(manifest, cancellationToken);
issues.AddRange(manifestValidationResult.Issues);

progress?.Report(new ValidationProgress(++currentStep, totalSteps, "Validating content files"));

// Use ContentValidator for full content validation (integrity + extraneous files)
try
{
var fullValidation = await contentValidator.ValidateAllAsync(sourcePath, manifest, progress, cancellationToken);
var contentIssues = isCombinedDirectory && gameType is not null
? fullValidation.Issues.Where(issue => !IsSiblingGameRootArchive(issue, gameType.Value))
: fullValidation.Issues;
issues.AddRange(contentIssues);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Manifest structure is validated twice per pass.

ContentValidator.ValidateManifestAsync calls ValidateManifestStructure(manifest) and returns those issues (Line 111-112). ContentValidator.ValidateAllAsync also calls ValidateManifestStructure(manifest) as its own first step, and its result is added via contentIssues at Line 123. The isCombinedDirectory filter at Line 120-121 only removes UnexpectedFile issues; it does not deduplicate manifest-structure issues. Every manifest-structure problem (missing Id, Name, Version, empty Files, and so on) is added to issues twice per pass, and up to four times total for a combined installation with two passes.

Remove the separate ValidateManifestAsync call, since ValidateAllAsync already performs the same structural check.

🐛 Proposed fix
-            progress?.Report(new ValidationProgress(++currentStep, totalSteps, "Core manifest validation"));
-
-            var manifestValidationResult = await contentValidator.ValidateManifestAsync(manifest, cancellationToken);
-            issues.AddRange(manifestValidationResult.Issues);
-
+            progress?.Report(new ValidationProgress(++currentStep, totalSteps, "Core manifest validation"));
+
             progress?.Report(new ValidationProgress(++currentStep, totalSteps, "Validating content files"));

Do you want me to also add a regression test that uses the real ContentValidator with a manifest missing Name, to confirm the issue count is 1, not 2?

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Installation-specific validations (directories, etc.)
var requiredDirs = manifest.RequiredDirectories ?? Enumerable.Empty<string>();
if (requiredDirs.Any())
{
if (installation.HasGenerals)
progress?.Report(new ValidationProgress(++currentStep, totalSteps, "Core manifest validation"));
var manifestValidationResult = await contentValidator.ValidateManifestAsync(manifest, cancellationToken);
issues.AddRange(manifestValidationResult.Issues);
progress?.Report(new ValidationProgress(++currentStep, totalSteps, "Validating content files"));
// Use ContentValidator for full content validation (integrity + extraneous files)
try
{
var fullValidation = await contentValidator.ValidateAllAsync(sourcePath, manifest, progress, cancellationToken);
var contentIssues = isCombinedDirectory && gameType is not null
? fullValidation.Issues.Where(issue => !IsSiblingGameRootArchive(issue, gameType.Value))
: fullValidation.Issues;
issues.AddRange(contentIssues);
}
progress?.Report(new ValidationProgress(++currentStep, totalSteps, "Core manifest validation"));
progress?.Report(new ValidationProgress(++currentStep, totalSteps, "Validating content files"));
// Use ContentValidator for full content validation (integrity + extraneous files)
try
{
var fullValidation = await contentValidator.ValidateAllAsync(sourcePath, manifest, progress, cancellationToken);
var contentIssues = isCombinedDirectory && gameType is not null
? fullValidation.Issues.Where(issue => !IsSiblingGameRootArchive(issue, gameType.Value))
: fullValidation.Issues;
issues.AddRange(contentIssues);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@GenHub/GenHub/Features/Validation/GameInstallationValidator.cs` around lines
108 - 124, Remove the separate contentValidator.ValidateManifestAsync call and
its manifestValidationResult.Issues addition from the validation flow. Keep
ValidateAllAsync as the single manifest and content validation entry point,
preserving the existing progress reporting and content-issue filtering.

// Use ContentValidator for full content validation (integrity + extraneous files)
try
{
var fullValidation = await contentValidator.ValidateAllAsync(sourcePath, manifest, progress, cancellationToken);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Nested ValidateAllAsync progress reports use a different Total than the outer loop.

This loop reports progress against totalSteps = passes.Count * 4 (Line 91). Line 119 forwards the same progress instance into contentValidator.ValidateAllAsync. ContentValidator.ValidateAllAsync reports its own progress on a Total = 3 scale (GenHub/GenHub/Features/Content/Services/ContentValidator.cs, lines 69-84). A single IProgress<ValidationProgress> subscriber receives interleaved reports with inconsistent Total values within the same validation run. The existing test ValidateAsync_WithProgressCallback_ReportsProgress asserts "All progress reports should have the same total. Found totals: [{string.Join(", ", allTotals)}]", so this contract is expected to hold; it currently only passes because that test mocks IContentValidator and never triggers the nested reports.

Pass null for progress in the ValidateAllAsync call, or wrap progress in an adapter that rescales nested reports into the outer step range.

🐛 Proposed minimal fix
-                var fullValidation = await contentValidator.ValidateAllAsync(sourcePath, manifest, progress, cancellationToken);
+                var fullValidation = await contentValidator.ValidateAllAsync(sourcePath, manifest, null, cancellationToken);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var fullValidation = await contentValidator.ValidateAllAsync(sourcePath, manifest, progress, cancellationToken);
var fullValidation = await contentValidator.ValidateAllAsync(sourcePath, manifest, null, cancellationToken);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@GenHub/GenHub/Features/Validation/GameInstallationValidator.cs` at line 119,
Update the nested content validation call in the outer validation loop to
prevent ContentValidator.ValidateAllAsync from reporting its own Total=3 values
through the outer progress subscriber. Pass null for progress, or use an adapter
that rescales nested ValidationProgress reports to the outer totalSteps scale
while preserving the outer progress contract.

Comment on lines +140 to +143
var requiredDirs = manifest.RequiredDirectories ?? Enumerable.Empty<string>();
if (requiredDirs.Any() && gameType is not null)
{
progress?.Report(new ValidationProgress(++currentStep, totalSteps, "Validating Zero Hour directories"));
issues.AddRange(await ValidateDirectoriesAsync(installation.ZeroHourPath, requiredDirs, cancellationToken));
issues.AddRange(await ValidateDirectoriesAsync(sourcePath, requiredDirs, cancellationToken));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## Locate file"
fd -a 'GameInstallationValidator\.cs$' . || true

echo "## Relevant file outline/size"
file="$(fd 'GameInstallationValidator\.cs$' GenHub 2>/dev/null | head -n1 || true)"
if [ -n "${file:-}" ]; then
  wc -l "$file"
  ast-grep outline "$file" || true
  echo "## Relevant sections"
  sed -n '1,220p' "$file" | cat -n
fi

Repository: community-outpost/GenHub

Length of output: 10885


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## Manifest and validator interfaces"
sed -n '1,220p' GenHub.Core/Interfaces/Manifest/IManifestProvider.cs | cat -n || true
sed -n '1,220p' GenHub.Core/Interfaces/Manifest/IManifest.cs | cat -n || true
sed -n '1,180p' GenHub.Core/Interfaces/Validation/IGameInstallationValidator.cs | cat -n || true

echo "## RequiredDirectories occurrences"
rg -n "RequiredDirectories|ValidateDirectoriesAsync|DirectoryMissing|MissingDirectory|InstallDirectory" GenHub GenHub.Core -S || true

echo "## ManifestProvider implementations"
fd -a '.*Manifest.*\.cs$' . | while read -r f; do echo "--- $f"; wc -l "$f"; rg -n "GetManifestAsync|RequiredDirectories|DirectoryMissing" "$f" || true; done

Repository: community-outpost/GenHub

Length of output: 26060


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="GenHub/GenHub/Features/Validation/GameInstallationValidator.cs"
test_file="GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Validation/GameInstallationValidatorTests.cs"
manifest_provider="GenHub/GenHub/Features/Manifest/ManifestProvider.cs"
content_manifest="GenHub/GenHub.Core/Models/Manifest/ContentManifest.cs"
content_validator="GenHub/GenHub/Features/Content/Services/ContentValidator.cs"
fs_validator="GenHub/GenHub/Features/Validation/FileSystemValidator.cs"

echo "## GameInstallationValidatorTests around missing directory test"
sed -n '200,270p' "$test_file" | cat -n

echo "## GameInstallationValidator fallback and directory validation"
sed -n '70,145p' "$file" | cat -n

echo "## ContentManifest RequiredDirectories declaration"
sed -n '90,115p' "$content_manifest" | cat -n

echo "## ContentValidator required directory handling"
sed -n '190,225p' "$content_validator" | cat -n

echo "## FileSystemValidator directory helper"
sed -n '25,55p' "$fs_validator" | cat -n

echo "## ManifestProvider manifest builds around required directories"
sed -n '120,170p' "$manifest_provider" | cat -n
sed -n '250,280p' "$manifest_provider" | cat -n

echo "## Static control-flow probe for required directory condition"
python3 - <<'PY'
from pathlib import Path
import re
content = Path("GenHub/GenHub/Features/Validation/GameInstallationValidator.cs").read_text()
fallback = bool(re.search(r'passes\.Count == 0\s*\{[^}]*passes\.Add\(\(null, installation\.InstallationPath\)\)', content, re.S))
directive = bool(re.search(r'if \(requiredDirs\.Any\(\) && gameType is not null\)', content))
method_calls = []
m = re.finditer(r'ValidateDirectoriesAsync\(([^)]*)\)', content)
for mm in m:
    method_calls.append(mm.group(1))
print({
    "fallback_adds_null_game_type": fallback,
    "directories_condition": directive,
    "require_game_type_non_null": "gameType is not null" in content,
    "validate_directories_calls": list(method_calls),
    "affected_passes_count_expr": "passes.Count * 4" in content
})
PY

Repository: community-outpost/GenHub

Length of output: 17919


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## GameInstallation.cs relevant constructor/slots"
file="$(fd 'GameInstallation\.cs$' GenHub GenHub.Core 2>/dev/null | head -n1 || true)"
if [ -n "${file:-}" ]; then
  wc -l "$file"
  sed -n '1,180p' "$file" | cat -n
fi

echo "## ManifestProvider fallback null gameType branch"
sed -n '235,350p' GenHub/GenHub/Features/Manifest/ManifestProvider.cs | cat -n

echo "## Content validator required dir validation effect"
rg -n "missing directories|MissingDirectory|DirectoryMissing|expectedDirectories|ContentValidator" GenHub/GenHub/Features/Content/Services/ContentValidator.cs GenHub/GenHub.Core/Models/Validation/ValidationIssue.cs GenHub/GenHub/Features/Validation/FileSystemValidator.cs -S

Repository: community-outpost/GenHub

Length of output: 9954


Avoid skipping required-directory validation in the fallback pass.

When passes falls back to (null, installation.InstallationPath), the generated manifest still adds "Data" and "Maps" to RequiredDirectories, but ValidateDirectoriesAsync is not called because the condition requires gameType is not null. As a result, an installation with missing Data or Maps in the fallback path can miss a DirectoryMissing issue even though the other validation steps run. Either validate RequiredDirectories for gameType is null, or remove the fallback manifest fallback so this invalidation never happens.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@GenHub/GenHub/Features/Validation/GameInstallationValidator.cs` around lines
140 - 143, Update the required-directory validation in the manifest validation
flow so ValidateDirectoriesAsync runs whenever requiredDirs contains entries,
including the fallback case where gameType is null. Preserve the existing
requiredDirs guard and ensure fallback manifests still report missing Data or
Maps directories.


var allFiles = Directory.GetFiles(directory, "*.exe", SearchOption.AllDirectories);
var allFiles = Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories)
.Where(path => ExecutableFileClassifier.IsLegacyLaunchCandidate(path, path));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: The XML doc remark above (lines 214–221) is now stale.

It states "a content-based (magic-byte) classification slots in at the classifier call without this site changing," implying this call still uses the name-only overload. Commit 3 of this PR changed it to the content-aware IsLegacyLaunchCandidate(path, path) overload — the site did change — and the analogous remark on GameClientDetector.GetLaunchCandidateFiles was updated to match, but this one was missed. A future reader could be misled into thinking content-based classification has not been applied here.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

&& inst.HasZeroHour
&& !string.IsNullOrEmpty(inst.GeneralsPath)
&& !string.IsNullOrEmpty(inst.ZeroHourPath)
&& Path.GetFullPath(inst.GeneralsPath).Equals(Path.GetFullPath(inst.ZeroHourPath), StringComparison.OrdinalIgnoreCase);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: The "combined directory" predicate is duplicated across three sites.

The same boolean (both games flagged and identical resolved paths) appears inline here, again in GameInstallationValidator.ValidateAsync, and as WindowsInstallationDetector.IsCombinedDirectory. If the definition of "combined" changes, all three must be kept in sync or detection, dedup, and validation will disagree — the validator's IsSiblingGameRootArchive suppression relies on matching the same definition the detectors used. Consider promoting this to a shared member (e.g. a GameInstallation.IsCombinedDirectory computed property) for a single source of truth.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Aug 3, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 2
Issue Details (click to expand)

SUGGESTION

File Line Issue
GenHub/GenHub/Features/Content/Services/Publishers/SuperHackersManifestFactory.cs 230 Stale XML doc remark claims content-based classification "slots in without this site changing," but commit 3 of this PR changed the call to the content-aware (path, path) overload; the analogous GameClientDetector remark was updated but this one was missed.
GenHub/GenHub/Features/GameClients/GameClientDetector.cs 68 "Combined directory" predicate is duplicated across GameClientDetector, GameInstallationValidator, and WindowsInstallationDetector.IsCombinedDirectory; a definition drift between them would desync detection, dedup, and the validator's sibling-archive suppression.
Files Reviewed (20 files)
  • GenHub/GenHub.Core/Constants/GameClientConstants.cs - 0 issues
  • GenHub/GenHub.Core/Constants/RetailArchiveConstants.cs - 0 issues
  • GenHub/GenHub.Core/Helpers/RetailArchiveClassifier.cs - 0 issues
  • GenHub/GenHub.Core/Interfaces/Manifest/IManifestProvider.cs - 0 issues
  • GenHub/GenHub.Core/Models/GameInstallations/GameInstallation.cs - 0 issues
  • GenHub/GenHub.Core/Models/GameInstallations/RetailArchiveClassification.cs - 0 issues
  • GenHub/GenHub.MacOS/GameInstallations/MacOSInstallationDetector.cs - 0 issues
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameClients/GameClientDetectorTests.cs - 0 issues
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Manifest/ManifestProviderTests.cs - 0 issues
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Validation/GameInstallationValidatorTests.cs - 0 issues
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Helpers/RetailArchiveClassifierTests.cs - 0 issues
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Models/GameInstallations/GameInstallationTests.cs - 0 issues
  • GenHub/GenHub.Tests/GenHub.Tests.MacOS/GameInstallations/MacOSInstallationDetectorTests.cs - 0 issues
  • GenHub/GenHub.Windows/GameInstallations/WindowsInstallationDetector.cs - 0 issues
  • GenHub/GenHub/Features/Content/Services/ContentValidator.cs - 0 issues
  • GenHub/GenHub/Features/Content/Services/Publishers/SuperHackersManifestFactory.cs - 1 issue
  • GenHub/GenHub/Features/GameClients/GameClientDetector.cs - 1 issue
  • GenHub/GenHub/Features/GameInstallations/InstallationPathResolver.cs - 0 issues
  • GenHub/GenHub/Features/Manifest/ManifestProvider.cs - 0 issues
  • GenHub/GenHub/Features/Validation/GameInstallationValidator.cs - 0 issues

Fix these issues in Kilo Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Enhancement New feature or request Testing Topic related to (unit) tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant