Skip to content

chore: release alpha 4 - #378

Open
bobtista wants to merge 205 commits into
mainfrom
development
Open

bobtista wants to merge 205 commits into
mainfrom
development

Conversation

@bobtista

@bobtista bobtista commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

Release GenHub Alpha 4 (v0.0.4).

This release delivers multi-platform support across Windows, Linux, and macOS, a unified downloads browser powered by CSV catalog registries and community providers (GenLauncher, ModDB, AODMaps, CNCLabs, TheSuperHackers), native BGFX and GeneralsOnline/EAC launching, profile and asset sharing via genhub:// protocol links, integrated tools (GenPatcher, GenHotkeys, Map Manager, Replay Manager with checkpoint resume and player takeover), a redesigned docked UI with dynamic themes, full localization parity (including Arabic RTL and Russian), and hardened workspace, CAS, and user data management.

What is included in alpha 4

Platforms and core runtime

  • Windows, Linux, and macOS native host support.
  • Linux packaging (AppImage, deb, tar) and Flatpak Steam installation detection.
  • Linux UI windowing fix to eliminate duplicate window frames and titlebars.
  • macOS app bundle packaging, application menu naming, and automated removal of the com.apple.quarantine attribute from game binaries.
  • Unix workspaces with hard links, symlink strategies, and atomic executable materialization (chmod +x preserved without mutating shared CAS blobs).
  • Case-insensitive executable resolution for client detection and manifest generation.
  • Game process monitor with full stderr diagnostic capture and forked process adoption on Unix and Windows.
  • Workspace delta reuse to speed up launch preparation by reusing unmodified workspace files.
  • Safe CAS lifecycle handling with fallback from unwritable pools and protection against destructive garbage collection.

Downloads and content providers

  • Unified downloads browser replacing legacy publisher cards, with search, category tabs, and filter panels.
  • CSV catalog registry system with automated discovery, resolution, validation, and offline caching.
  • Publisher catalog subscription subsystem and update confirmation UI.
  • GenLauncher publisher provider supporting catalog browsing, downloads, redirect probing, and archive normalization.
  • ModDB provider with Playwright headless scraper for downloads requiring browser verification.
  • AODMaps and CNCLabs community map providers with filter panels.
  • TheSuperHackers provider with GeneralsGamePatch2 download support.
  • Parallel range chunk downloader for Velopack updates.
  • Bounded archive extraction with expansion size limits and cancellation propagation.

Profiles, installations, and game clients

  • First-run setup wizard with automatic installation detection and profile generator for downloaded content.
  • Support for retail game installations (CD, EA App, Steam, Flatpak Steam) and custom manual game registrations.
  • Custom installation directory configuration with in-app data migration.
  • Dual retail and non-retail community patch game client support.
  • Native BGFX client launch support against retail archives.
  • GeneralsOnline launching with Easy Anti-Cheat (EAC) bootstrapper integration and registry setup.
  • Bundled ZH_Generals retail archive root discovery on Linux and macOS.
  • Safe user-data deployment, backup, and restoration for GeneralsOnline game data patches and custom content.
  • Local content import overlay with progress reporting and .ctr archive normalization.
  • Active profile ID persistence across application restarts.
  • Pre-launch dependency check to flag missing catalog files before launching.
  • Runtime content hot-swapping during active game sessions.

URI scheme and sharing (genhub://)

  • Registered genhub:// protocol handler on Windows with command line argument parsing.
  • Profile sharing via genhub://profile/... links with an interactive inspection dialog.
  • Map and replay upload and link sharing via genhub://map/... and genhub://replay/... links.
  • Secure UploadThing gateway for community asset uploads with HMAC deletion receipts.

Tools (GenPatcher, GenHotkeys, Map and Replay managers)

  • GenPatcher integration with ActionSet orchestration for automated game fixes.
  • GenHotkeys integration with key binding configuration, addon manifest creation, and icon overlays.
  • Map Manager with preview caching, filter options, and user directory synchronization.
  • Replay Manager with replay header parsing, map CRC lookup, and match statistics.
  • Replay checkpoint saving, resuming, and player takeover during replay inspection.
  • Persistent tools sidebar that preserves tool selection across navigation.

User interface, themes, and localization

  • Docked resizable sidebar layout with an interactive splitter.
  • Centered navigation pill tabs and reorganized header controls.
  • Dynamic color theme tokens, dark surfaces, and fixed scrollbar thumb dragging behavior.
  • Complete UI localization with 1:1 resx parity across all views, including Arabic (with RTL layout) and Russian.
  • Settings sidebar scroll-spy and smooth section scrolling.
  • Resilient log viewer and log clearing in Settings.
  • Game window transition speed multiplier setting.
  • GitHub Device Code OAuth login ("Sign in with GitHub") replacing manual Personal Access Tokens.
  • Changelog viewer with local caching, release drafter integration, and post-update notifications.

Validation and test guide

Automated and CI checks

  • CI builds and tests pass on Windows, Linux, and macOS
  • Engine launch smoke test passes
  • Solution builds with zero compiler or analyzer warnings
  • SonarCloud code quality analysis clean
  • Final release candidate SHA is frozen
  • Published release assets and update metadata are valid

Platform packages and install verification

  • Packaged Windows application installs and starts
  • Packaged macOS application starts and displays GenHub in menu bar
  • Packaged Linux application installs and starts (amd64 container, Xvfb)
  • Verify Linux packaging formats (AppImage, deb, tar) install and run across Ubuntu, Fedora, and Arch
  • Verify Linux window decorations display a single titlebar without duplicate borders
  • Verify macOS launches game executables without Gatekeeper quarantine blocks

Upgrade and data migration

  • Alpha 3 configuration and manifests migrate correctly
  • Alpha 3 profiles migrate correctly, including custom data paths
  • Custom install directory migration moves CAS storage and workspaces cleanly
  • Verify profile deactivation and activation restores user documents without file collisions
  • Verify "Clear Logs" in Settings deletes log files without file lock errors

Setup wizard and installations

  • Real installations are detected (Retail CD, EA App, Steam, Flatpak Steam)
  • Manually added game installations register, validate, and persist across restarts
  • Setup wizard detects installations and offers profile creation for existing content
  • Verify setup wizard offers "Create Profile" for downloaded content lacking a profile
  • Verify attempting to delete a game installation with active dependent clients displays a blocking warning

Downloads browser and content providers

  • Downloads browser discovers, filters, and downloads CSV catalog packages
  • Subscribe and unsubscribe from publisher catalogs in the catalog UI
  • Browse and download packages from ModDB provider (verifying Chromium headless acquisition)
  • Browse and download packages from GenLauncher provider
  • Browse and download maps from AODMaps and CNCLabs providers
  • Download GeneralsGamePatch2 from TheSuperHackers provider
  • Verify download cancellations abort cleanly without leaving truncated files in CAS
  • Verify parallel range chunk downloader during Velopack app update checks

Launching and game profiles

  • Profiles launch standard retail Generals and Zero Hour installations
  • Launch native BGFX client against retail archive roots on Windows, Linux, and macOS
  • Verify Zero Hour bundled Generals archive root resolves on Linux and macOS
  • Launch GeneralsOnline with EAC bootstrapper, exit, and verify relaunch without reinstalling
  • Verify GeneralsOnline game data patch deploys to user documents and restores original files on removal
  • Verify pre-launch dependency check blocks launch and reports missing catalog files
  • Verify runtime content hot-swapping switches profile content while game is running
  • Verify active profile selection persists after restarting the application
  • Import archive through Add Local Content dialog, verify .ctr normalization, and confirm progress overlay

URI protocol links and sharing (genhub://)

  • Windows genhub:// URL handler registers during setup
  • Open a genhub://profile/... link and verify inspection dialog displays accurate profile components
  • Import profile from inspection dialog and verify the new profile launches
  • Upload a map or replay to generate a genhub:// link
  • Open a genhub://map/... or genhub://replay/... link to verify download and import
  • Test file deletion via gateway HMAC deletion receipt

Tools (GenPatcher, GenHotkeys, Map and Replay managers)

  • Execute GenPatcher ActionSets on target game installations
  • Open GenHotkeys, modify key bindings, and verify addon manifest and icon generation
  • Verify Map Manager displays map previews, metadata, and deploys maps to user directories
  • Verify Replay Manager parses replay headers, matches map CRC, and displays game stats
  • Verify replay checkpoint save, resume, and player takeover in Replay Manager
  • Verify tools sidebar preserves the active tool across tab navigation

UI, settings, and localization

  • Switch between dynamic color themes and verify dark surface contrast
  • Verify scrollbar thumb dragging does not drift toward top-left
  • Click Settings sidebar sections and verify scroll-spy jumps to the matching section
  • Switch language to Arabic and verify RTL layout, string replacement, and font rendering
  • Switch language to Russian and verify string coverage without missing action keys
  • Adjust game window transition speed multiplier in Settings and verify launch behavior
  • Sign in with GitHub via Device Code flow and confirm authenticated profile state
  • Open changelog viewer, verify cached release notes, and check post-update prompt

undead2146 and others added 30 commits December 28, 2025 19:38
…trols

- Added `NullableDecimalToIntConverter` to handle data conversion between View and ViewModel.
- Defined a new "phantom" style for `NumericUpDown` to match the existing minimalist UI.
- Replaced `TextBlock` value displays with `NumericUpDown` controls for Gamma, Audio volumes, Font sizes, and Camera settings.
- Adjusted Grid column widths to accommodate interactive input fields.
Added manual folder picker for Retail GameInstallations without registry
…227)

Fixed GameSettings not persisting whenever the profile was saved.
… resolution (#225)

* fix(deps): ensure disabled game installation is auto-resolved when required by client

* feat(local): add GameType selection and validation for local content

* feat(game-profiles): hide GameInstallation from UI
…ate detection (#228)

Upon launching a GameProfile that has a GeneralsOnline GameClient, a check for an update will be made. 
When an update is available it will download the new release, remove the old GameClient and MapPack, reconcile the profiles with the new release, then continue with launching the game.
Users can now subscribe to development branches to receive notifications for every new push or update
…xternal JSON (#205)

This commit lays the foundation for the Provider Defition framework whereby publishers can become ContentProviders in GenHub.
… management (#235)

- Added a GameType filter for Generals and Zero Hour.
- Updated the selected ContentType button to highlight when active.
- Centered all ContentItems.
- Organized ContentItems into a brick-style layout.
- Replaced the add/remove buttons with a click action on the ContentItem itself.
- Set the "Delete" button to be visible only on hover.
Added a section under settings for the following data directories; workspace, profiles, manifests and CAS-pool,
…234)

Removed old local content dialog properties and methods from GameProfileSettingsViewModel.
Enhanced UI with drag-and-drop support for file and folder imports.
…age and multi-instance debugging support (#239)

*   Added persistent storage for manual game installations and multi-instance debugging support.
*   Improved update reliability with HTTP retries, GitHub API caching, and enhanced error handling.
*   Refactored constants and implemented automated cleanup for orphaned workspace directories.
…port and GitHub rate limit tracking (#237)

This update introduces a persistent notification feed with history tracking and a redesigned bell UI featuring dynamic unread badges. It adds support for multi-action notifications with styled buttons and integrates a GitHub rate limit tracker to provide real-time API usage warnings. The core notification architecture has been refactored into record types for better immutability, alongside service enhancements for managing read states and history. Additionally, the UI features a new custom title bar and redesigned toasts, backed by comprehensive unit tests to ensure thread safety and reliability.
…y launching (#241)

* feat: Implement SteamLauncher for game directory preparation and proxy launching

- Added SteamLauncher service to manage game directory preparation for Steam-tracked profiles.
- Introduced ProxyConfig for configuring the proxy launcher with target executable and working directory.
- Implemented methods for deploying the proxy launcher and creating necessary configuration files.
- Enhanced manifest generation to support backup executable handling from the Steam Proxy Launcher.
- Updated SteamManifestPatcher to improve executable validation logic.
- Refactored HardLinkStrategy to prioritize file deduplication based on content type.
- Improved error handling and logging throughout the game installation and manifest processes.
- Updated documentation to reflect changes in detection and manifest creation workflows.

* fix: resolve proxy launcher not replacing generals.exe

* chore: resolve greptile comments

* fix: resolve GameProfileSettingsWindow drag to resize from fullscreen
)

* feat: add GitHub Pages deployment workflow and landing page assets

* feat: Add Replay Manager tool with drag-and-drop functionality and file management
- Implemented ReplayManagerView for managing replay files.
- Added drag-and-drop support for importing replay files (.rep, .zip).
- Enhanced ToolsViewModel to include Replay Manager services.
- Updated UI styles for better visual consistency.
- Introduced EnumToBoolConverter and EqualsToConverter for improved data binding.
- Added documentation for Replay Manager features and usage.
- Registered Replay Manager services in the dependency injection module.

* feat(tools): enhance map/replay managers and refine tools sidebar ui

* feat(tools): enhance map/replay managers and refine tools sidebar ui

* feat(tools): enhance map/replay managers and refine tools sidebar ui

* fix: resolve build version mismatch and the add local content DragDrop not working admin
…odding utilities (#247)

Users can now add individual executables, or tools to be ran in a gameprofile. 
Tools or executables like WorldBuilder.exe, WNDeditor, ParticleEditor, Discord.exe, Notepad.exe, or any other program or tool they desire.
@github-actions

This comment was marked as duplicate.

@github-actions

This comment was marked as resolved.

@github-actions

Copy link
Copy Markdown

Incremental PR Reviewer Guide 🔍

⏮️ Review for commits since previous PR-Agent review Starting from commit d260755.

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 5 🔵🔵🔵🔵🔵
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Localization & constants violation

This PR removes the centralized GameClientConstants.WizardDescriptionTemplates and replaces them with inline, hardcoded English user-facing strings: "Community Patch (Retail)" / "Community Patch (Non-Retail)" titles, "Download and install Community Patch (Retail)." descriptions, and the wizard item suffix " Not compatible with retail 1.04 zero hour.". The new FormatCreateProfileDescription / FormatUpdateProfileDescription / FormatDetectedInstallDescription helpers also build English sentences via interpolation instead of resx keys. These strings are rendered in the Setup Wizard dialog shown to users, violating the repo rule that all user-facing text lives in Strings.resx (with ar/ru parity) and that constants be centralized under GenHub.Core.Constants. Arabic and Russian users will see untranslated wizard text.

var cpRetailDescription = string.IsNullOrEmpty(cpRetailCleanVersion)
    ? "Download and install Community Patch (Retail)."
    : $"Download and install Community Patch (Retail) {cpRetailCleanVersion}.";

var cpNonRetDescription = string.IsNullOrEmpty(cpNonRetCleanVersion)
    ? "Download and install Community Patch (Non-Retail)."
    : $"Download and install Community Patch (Non-Retail) {cpNonRetCleanVersion}.";
Possible data loss

The new TryCopy deletes the existing target file before copying (File.Delete followed by File.Copy(..., overwrite: false)). If the copy then fails (disk full, transient lock, permission issue), the previously materialized file in the target directory has already been destroyed and only a failure result remains. The old code used File.Copy(..., overwrite: true), which leaves the old file intact when the copy fails. Concrete scenario: re-materializing content into a nearly-full staging/workspace volume deletes the current file and then fails, leaving the workspace without that file. Consider copying to a temp name and swapping, or keeping overwrite: true semantics so failure does not destroy existing content.

private static bool TryCopy(string sourcePath, string targetPath)
{
    try
    {
        if (File.Exists(targetPath))
        {
            File.Delete(targetPath);
        }

        // Copy semantics keep CAS objects immutable. Staging supports in-place
        // overwrites, so a hard link here would let edits mutate shared CAS content.
        File.Copy(sourcePath, targetPath, overwrite: false);
        return true;
    }
    catch (IOException)
    {
        return false;
    }
    catch (UnauthorizedAccessException)
    {
        return false;
    }
}
Swallowed cancellation

The new GetLatestCommunityPatchVersionsAsync wraps discovery in catch (Exception ex) and logs it as "Failed to retrieve latest Community Patch versions". Because OperationCanceledException derives from Exception, a user-initiated cancellation during DiscoverAsync is swallowed, logged as a benign version-lookup warning, and the wizard continues with empty versions instead of propagating the cancellation. Per repo rules, cooperative cancellation must propagate and generic Exception catches are only allowed at top-level boundaries; add catch (OperationCanceledException) { throw; } before the generic catch.

catch (Exception ex)
{
    logger.LogWarning(ex, "Failed to retrieve latest Community Patch versions");
}

return (string.Empty, string.Empty);
Hardcoded UI string

In ResolveCompatibilityBadge, the Generals-case tooltip is hardcoded as "Compatible with retail 1.08 (EXE CRC match)" with no GetLocalizedOrDefault fallback, while the adjacent Zero Hour tooltip and both badge texts are localized via LocalizationConverterHelper. This is a user-facing profile-list tooltip that will not translate, and it bypasses the localization helper pattern used a few lines below, breaking parity for the Generals case.

CompatibilityTooltip = profile.GameClient.GameType == GameType.Generals
    ? "Compatible with retail 1.08 (EXE CRC match)"
    : LocalizationConverterHelper.GetLocalizedOrDefault(
        loc,
        "GameProfiles.Tooltip.RetailCompatible",
        "Compatible with retail 1.04 (EXE CRC match)");

⚠️ Review coverage: The following files were not included in this review because of the token budget:

  • GenHub/GenHub.Core/Constants/DownloadNotificationConstants.cs
  • GenHub/GenHub.Linux/Infrastructure/SingleInstance/LinuxSingleInstanceManager.cs
  • GenHub/GenHub/Infrastructure/Converters/LocalizationConverterHelper.cs
  • GenHub/GenHub/Features/Tools/GenHotkeys/ViewModels/GenHotkeysViewModel.cs
  • GenHub/GenHub.Linux/Features/Shortcuts/LinuxShortcutService.cs
  • GenHub/GenHub.Core/Interfaces/Providers/IProviderDefinitionLoader.cs
  • GenHub/GenHub.Core/Models/Common/UploadHistoryItem.cs
  • GenHub/GenHub/Features/Tools/Views/ShareLinksDialog.axaml.cs
  • GenHub/GenHub.Windows/Features/Shortcuts/WindowsShortcutService.cs
  • GenHub/GenHub/Common/Services/DownloadUrlValidator.cs
  • GenHub/GenHub.Core/Models/GameProfile/SetupWizardResult.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/ViewModels/SettingsViewModelTests.cs
  • GenHub/GenHub.Core/Helpers/CommandLineParser.cs
  • GenHub/GenHub.MacOS/Features/Shortcuts/MacOSShortcutService.cs
  • GenHub/GenHub.Core/Constants/ScrollSpyConstants.cs
  • GenHub/GenHub.Core/Constants/ValidationLimits.cs
  • GenHub/GenHub.Core/Helpers/PathHelper.cs
  • GenHub/GenHub/Features/Content/Services/SuperHackers/SuperHackersUpdateService.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Helpers/PathHelperTests.cs
  • GenHub/GenHub/Features/Content/Services/ContentDiscoverers/FileSystemDiscoverer.cs
  • GenHub/GenHub/Infrastructure/DependencyInjection/UnixServicesModule.cs
  • GenHub/GenHub/Features/Downloads/ViewModels/CreateProfileOptionViewModel.cs
  • GenHub/GenHub.Core/Constants/IpcCommands.cs
  • GenHub/GenHub/Features/Content/Services/Publishers/SuperHackersManifestFactory.cs
  • GenHub/GenHub/Features/Content/Services/CommunityOutpost/CommunityOutpostResolver.cs
  • GenHub/GenHub.Core/Models/Tools/UploadRecord.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/Services/DependencyResolverCatalogIdentityTests.cs
  • GenHub/GenHub/Features/Settings/ViewModels/SettingsViewModel.cs
  • GenHub/GenHub/Features/Tools/ViewModels/UploadHistoryItemViewModel.cs
  • GenHub/GenHub.Core/Interfaces/Common/IDownloadUrlValidator.cs
  • GenHub/GenHub.Core/Constants/LogMessages.cs
  • GenHub/GenHub.Core/Constants/ToolConstants.cs
  • GenHub/GenHub.Core/Constants/DownloadDefaults.cs
  • GenHub/GenHub.Core/Helpers/ToolShareTarget.cs
  • GenHub/GenHub.Core/Helpers/ManifestJsonOptions.cs
  • GenHub/GenHub/Features/Downloads/ViewModels/Filters/SuperHackersFilterViewModel.cs
  • scripts/fetch-changelogs.py
  • scripts/generate_crc_catalog.py
  • GenHub/GenHub/Resources/Localization/Strings.ru.resx
  • GenHub/GenHub/Resources/Localization/Strings.ar.resx
  • GenHub/GenHub/Features/Tools/MapManager/Views/MapManagerView.axaml
  • GenHub/GenHub/Resources/Localization/Strings.resx
  • GenHub/GenHub/Features/Tools/Views/ToolIcons.axaml
  • GenHub/GenHub/Features/Tools/Views/ShareLinksDialog.axaml
  • GenHub/GenHub/Features/Tools/ReplayManager/Views/ReplayManagerView.axaml
  • GenHub/GenHub/Resources/crc-mapping.json
  • GenHub/GenHub/Features/AppUpdate/Views/UpdateNotificationView.axaml
  • GenHub/GenHub/Features/GameProfiles/Views/GameProfileCardView.axaml
  • GenHub/GenHub/Features/Tools/Views/ToolActionBarStyles.axaml
  • GenHub/GenHub/Features/GameProfiles/Views/GameProfileLauncherView.axaml
    ... and 5 more

@github-actions

Copy link
Copy Markdown

Incremental PR Reviewer Guide 🔍

⏮️ Review for commits since previous PR-Agent review Starting from commit ca3fd6b.

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 5 🔵🔵🔵🔵🔵
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Missing CancellationToken

The new ScrubDeletedManifestIdsFromProfilesAsync and ScrubDeletedManifestIdsFromProfileAsync methods issue GetAllProfilesAsync() and UpdateProfileAsync(...) without accepting or propagating a CancellationToken. This violates the repository's async rule that every long-running task must accept and propagate a token. The method loops over all profiles and performs a sequential profile update per affected profile, so with many profiles (or a slow profile store) the deletion flow cannot be cancelled and will keep running after the user navigates away or the dialog closes. Add a CancellationToken parameter and forward it to both manager calls (threading it through from DeleteManifestsAsync).

private async Task ScrubDeletedManifestIdsFromProfilesAsync(HashSet<string> deletedIds, bool showToast)
{
    var profilesResult = await _profileManager.GetAllProfilesAsync();
    if (!profilesResult.Success || profilesResult.Data == null)
    {
        _logger.LogWarning(
            "Failed to enumerate profiles while scrubbing deleted manifest IDs: {Error}",
            profilesResult.FirstError);
        if (showToast)
        {
            var enumerationFormat = _localizationService?.GetString("Settings.Manifests.ScrubFailed.EnumerationMessage") ?? "The profile list could not be loaded, so deleted manifests may still be referenced by profiles. Those profiles may fail to launch until updated.";
            _notificationService.ShowWarning(
                GetScrubFailedTitle(),
                enumerationFormat,
                (int)TimeIntervals.NotificationHideDelay.TotalMilliseconds);
        }

        return;
    }

    var failedProfileNames = new List<string>();
    foreach (var profile in profilesResult.Data)
    {
        var scrubbed = await ScrubDeletedManifestIdsFromProfileAsync(profile, deletedIds);
        if (!scrubbed)
        {
            failedProfileNames.Add(profile.Name);
        }
    }

    if (showToast && failedProfileNames.Count > 0)
    {
        var scrubFailedFormat = _localizationService?.GetString("Settings.Manifests.ScrubFailed.Message") ?? "Deleted manifests could not be removed from {0} profile(s): {1}. Those profiles may fail to launch until updated.";
        _notificationService.ShowWarning(
            GetScrubFailedTitle(),
            string.Format(CultureInfo.InvariantCulture, scrubFailedFormat, failedProfileNames.Count, string.Join(", ", failedProfileNames)),
            (int)TimeIntervals.NotificationHideDelay.TotalMilliseconds);
    }
}

private string GetScrubFailedTitle()
{
    return _localizationService?.GetString("Settings.Manifests.ScrubFailed.Title") ?? "Profile Update Incomplete";
}

private async Task<bool> ScrubDeletedManifestIdsFromProfileAsync(GameProfile profile, HashSet<string> deletedIds)
{
    if (profile.EnabledContentIds == null || !profile.EnabledContentIds.Any(id => deletedIds.Contains(id)))
    {
        return true;
    }

    var updatedContentIds = profile.EnabledContentIds
        .Where(id => !deletedIds.Contains(id))
        .ToList();

    var updateRequest = new UpdateProfileRequest
    {
        EnabledContentIds = updatedContentIds,
    };
    var updateResult = await _profileManager.UpdateProfileAsync(profile.Id, updateRequest);
    if (!updateResult.Success)
    {
        _logger.LogWarning(
            "Failed to scrub deleted manifest IDs from profile {ProfileName} ({ProfileId}): {Error}",
            profile.Name,
            profile.Id,
            updateResult.FirstError);
        return false;
    }

    _logger.LogInformation("Scrubbed deleted manifest IDs from profile {ProfileName} ({ProfileId})", profile.Name, profile.Id);
    return true;
Duplicated preflight logic

The new CheckManifestCasFilesAsync helper and the rewritten PreflightCasCheckAsync body are near-identical copies of CheckManifestCasFilesAsync and VerifyCasContentAvailabilityAsync added to ProfileLauncherFacade.cs in this same PR — including the identical user-facing failure string "Missing {count} required CAS objects (...). Content must be downloaded before launching.". Beyond maintainability, the SonarCloud Quality Gate fails when duplicated lines on new code exceed 3.0%, which this duplication risks tripping. Extract the shared missing-CAS scan (helper + aggregation + failure message) into one shared helper (e.g., alongside the new CasServiceExtensions.GetMissingRequiredCasFilesAsync) and have both launch paths call it.

private async Task CheckManifestCasFilesAsync(ContentManifest manifest, List<string> missingFiles, CancellationToken cancellationToken)
{
    var manifestDisplayName = !string.IsNullOrWhiteSpace(manifest.Name) ? manifest.Name : manifest.Id.Value;
    var missingCasFiles = await casService.GetMissingRequiredCasFilesAsync(manifest, cancellationToken).ConfigureAwait(false);
    foreach (var file in missingCasFiles)
    {
        logger.LogWarning(
            "[Preflight CAS] Missing CAS object {Hash} for file {RelativePath} in manifest {ManifestId} ({ManifestName})",
            file.Hash,
            file.RelativePath,
            manifest.Id,
            manifest.Name);
        missingFiles.Add($"{manifestDisplayName} ({file.RelativePath})");
    }
}

/// <summary>
/// Performs a preflight check to ensure all CAS content required by the manifests is available.
/// </summary>
/// <param name="manifests">The manifests to check.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A result indicating success or failure.</returns>
private async Task<OperationResult<bool>> PreflightCasCheckAsync(IEnumerable<ContentManifest> manifests, CancellationToken cancellationToken)
{
    var missingFiles = new List<string>();
    foreach (var manifest in manifests)
    {
        await CheckManifestCasFilesAsync(manifest, missingFiles, cancellationToken).ConfigureAwait(false);
    }

    if (missingFiles.Count > 0)
    {
        var distinctMissing = missingFiles.Distinct().ToList();
        return OperationResult<bool>.CreateFailure($"Missing {distinctMissing.Count} required CAS objects ({string.Join(", ", distinctMissing.Take(5))}). Content must be downloaded before launching.");
Bare catch clause

The new GetObjectSize helper uses a bare catch { } with only a comment, silently swallowing every exception type (including UnauthorizedAccessException, IOException, etc.) with no logging. This violates the repository's exception-handling baseline ("never catch generic Exception; always catch specific exception types"). Silent swallowing here also means stat sizes can be silently underreported with no diagnostic trail. Catch the specific I/O-related exception types and log at debug level, consistent with how the rest of the service reports failures.

private static long GetObjectSize(ICasStorage poolStorage, string hash)
{
    try
    {
        var objectPath = poolStorage.GetObjectPath(hash);
        if (File.Exists(objectPath))
        {
            return new FileInfo(objectPath).Length;
        }
    }
    catch
    {
        // Skip files that can't be accessed
    }

    return 0;
}

⚠️ Review coverage: The following files were not included in this review because of the token budget:

  • GenHub/GenHub/Resources/Localization/Strings.resx
  • GenHub/GenHub/Features/GameProfiles/Views/GameProfileContentSettingsView.axaml
  • GenHub/GenHub/Features/GameProfiles/Views/GameProfileSettingsWindow.axaml
  • GenHub/GenHub.Linux/GenHub.Linux.csproj
  • GenHub/GenHub.MacOS/GenHub.MacOS.csproj
  • GenHub/GenHub.Windows/GenHub.Windows.csproj
  • GenHub/GenHub/Features/GameProfiles/Views/GameSettingsView.axaml
  • docs/dev/constants.md
  • GenHub/GenHub/Features/GameProfiles/Views/GameProfileGeneralSettingsView.axaml
  • docs/features/steam-proxy-launcher.md
  • GenHub/GenHub.ProxyLauncher/GenHub.ProxyLauncher.csproj
  • docs/features/storage.md
  • docs/dev/result-pattern.md
  • GenHub/GenHub/App.axaml
  • GenHub/GenHub.Tests/GenHub.Tests.Core/GenHub.Tests.Core.csproj

@github-actions

Copy link
Copy Markdown

Incremental PR Reviewer Guide 🔍

⏮️ Review for commits since previous PR-Agent review Starting from commit 0025e1b.

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Unverified removal result breaks partial-scrub correctness

The new cancellation handling relies on removedCount to determine which manifest IDs were "actually deleted" (see ScrubPartiallyRemovedManifestsAsync, which scrubs orderedIds.Take(removedCount)). However, the return value of _manifestPool.RemoveManifestAsync (an OperationResult<bool>) is ignored and removedCount++ runs unconditionally. If a removal fails without throwing — realistic when manifest files are locked, e.g., the game is running on Windows — that manifest is still counted as removed. If cancellation then occurs after that point, the partial scrub silently strips a still-existing manifest's ID from all profiles' EnabledContentIds, so profiles lose a valid content reference and may fail to launch. The fix is to check the result and only increment (or track succeeded IDs) when RemoveManifestAsync reports success.

await _manifestPool.RemoveManifestAsync(manifest.Id, cancellationToken: cancellationToken);
removedCount++;

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

Incremental PR Reviewer Guide 🔍

⏮️ Review for commits since previous PR-Agent review Starting from commit 17b8b51.

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Result Pattern Violation

The new delete-flow helpers GetDownloadedManifestAsync, FindProfilesUsingManifestAsync, and FindTypeDependentsAsync convert failed OperationResults from the manifest pool and profile manager (expected operational failures, e.g. a transient pool or profile-store read failure during delete) into thrown InvalidOperationException, which DeleteDownloadAsync then handles via a bare catch (Exception). This uses exceptions for predictable control flow, violating the repository Result-pattern rule (expected failures must flow as OperationResult/domain results; exceptions are only for contract violations and cancellation) and the ban on generic catch (Exception) outside top-level boundaries. Functionally the failure still surfaces as an error toast, but the throw/catch round-trip is untyped, brittle if an unrelated exception of the same type occurs, and harder to test. Consider returning the failed result and branching on Success.

private async Task<ContentManifest?> GetDownloadedManifestAsync(string manifestId)
{
    var manifestResult = await manifestPool.GetManifestAsync(ManifestId.Create(manifestId), _cts.Token);
    if (!manifestResult.Success)
    {
        throw new InvalidOperationException(manifestResult.FirstError ?? "Unable to read the stored manifest.");
    }

    return manifestResult.Data;
}
Redundant Backing Field

DownloadedContentFilterViewModel injects ILocalizationService through a classic constructor and stores it in a private readonly backing field (_localizationService, line 22) that is then accessed throughout the class. Repository rule 1 requires a C# 12 primary constructor when dependencies are injected, with the parameter accessed directly and no redundant backing field (as done correctly in ContentArtworkService and DownloadedContentDiscoverer): public sealed partial class DownloadedContentFilterViewModel(ILocalizationService? localizationService = null) : FilterPanelViewModelBase, IDisposable. Low severity, but it breaks the enforced convention.

public DownloadedContentFilterViewModel(ILocalizationService? localizationService = null)
{
    _localizationService = localizationService;
    ContentTypeFilters = CreateContentTypeFilters();
    if (_localizationService != null)
    {
        _localizationService.PropertyChanged += OnLocalizationChanged;
    }
}

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

Labels

Chore Routine maintenance, builds, and dependencies Release PR’s containing changes for the next release. Review effort 4/5

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants