Skip to content

Conversation

@undead2146
Copy link
Owner

@undead2146 undead2146 commented Jan 3, 2026

Summary by CodeRabbit

  • New Features

    • Local content import window with staging preview and map auto-organization
    • GitHub manifest generation for extracted packages
    • Publisher theme colors surfaced in content metadata
  • Improvements

    • Per-file download progress and content-acquired notifications
    • Threaded progress reporting through content pipeline and a new "storing" acquisition phase
    • Dependency validation respects optional dependencies
  • Refactor

    • Main view model converted to use injected dependencies and SelectedTab
  • UI Updates

    • Download progress indicator color changed to purple; updated map/replay UI text and global accent color

✏️ Tip: You can customize this high-level summary in your review settings.

…ommunity-outpost#234)

Removed old local content dialog properties and methods from GameProfileSettingsViewModel.
Enhanced UI with drag-and-drop support for file and folder imports.
@netlify
Copy link

netlify bot commented Jan 3, 2026

Deploy Preview for generalshub ready!

Name Link
🔨 Latest commit c8788f0
🔍 Latest deploy log https://app.netlify.com/projects/generalshub/deploys/696003baedd5820008cd7d2e
😎 Deploy Preview https://deploy-preview-11--generalshub.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link

coderabbitai bot commented Jan 3, 2026

Important

Review skipped

Too many files!

134 files out of 284 files are above the max files limit of 150.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

📝 Walkthrough

Walkthrough

Adds publisher/theme color metadata and threads progress reporting through content download, extraction, and manifest storage; introduces a GitHub manifest factory; adds a local-content import UI/VM; updates many call sites for the new AddManifestAsync progress parameter; and relaxes CI token handling for fork PRs.

Changes

Cohort / File(s) Summary
Theme Color & UI constants
GenHub/GenHub.Core/Constants/CommunityOutpostConstants.cs, GenHub/GenHub.Core/Constants/GeneralsOnlineConstants.cs, GenHub/GenHub.Core/Constants/SuperHackersConstants.cs, GenHub/GenHub.Core/Constants/UiConstants.cs
Added publisher-specific theme color constants and a global AccentColor.
Content progress types & messages
GenHub/GenHub.Core/Constants/ContentConstants.cs, GenHub/GenHub.Core/Models/Content/ContentAcquisitionPhase.cs, GenHub/GenHub.Core/Models/Content/ContentAcquiredMessage.cs
Added ProgressStepStoring, enum StoringInCas, and ContentAcquiredMessage.
Manifest metadata
GenHub/GenHub.Core/Models/Manifest/ContentMetadata.cs
Added nullable ThemeColor property to manifest metadata.
Manifest pool API & call sites
GenHub/GenHub.Core/Interfaces/Manifest/IContentManifestPool.cs, GenHub/GenHub/Features/Manifest/ContentManifestPool.cs, GenHub/GenHub/Features/Manifest/ManifestProvider.cs, various consumers (.../Deliverer.cs, .../GameInstallationService.cs, .../GameProfiles/*, tests)
Added optional IProgress<ContentStorageProgress>? progress = null to AddManifestAsync; updated call sites to pass null or progress adapters.
GitHub content pipeline & factory
GenHub/GenHub/Features/Content/Services/GitHub/GitHubContentDeliverer.cs, GenHub/GenHub/Features/Content/Services/Publishers/GitHubManifestFactory.cs, GenHub/GenHub/Infrastructure/DependencyInjection/ContentPipelineModule.cs
New GitHubManifestFactory; per-file download progress, manual zip extraction with extraction progress, storage progress adapters wired to manifest pool; DI registration.
Publisher manifest factories
GenHub/GenHub/Features/Content/Services/CommunityOutpost/CommunityOutpostManifestFactory.cs, GenHub/GenHub/Features/Content/Services/GeneralsOnline/GeneralsOnlineManifestFactory.cs, GenHub/GenHub/Features/Content/Services/Publishers/SuperHackersManifestFactory.cs
Populate ThemeColor on generated manifests from publisher constants.
Game profiles theming & API changes
GenHub/GenHub.Core/Interfaces/GameProfiles/IGameClientProfileService.cs, GenHub/GenHub/Features/GameProfiles/Services/GameClientProfileService.cs
Added optional string? themeColor = null to profile creation APIs; service resolves/prefers provided theme color and added game-client-aware theme lookup.
Downloads UI & messaging
GenHub/GenHub/Features/Downloads/ViewModels/PublisherCardViewModel.cs, GenHub/GenHub/Features/Downloads/Views/PublisherCardView.axaml
Emit ContentAcquiredMessage after download, refine progress formatting, and change progress bar color to #A15DF3.
Local content import (VM & UI)
GenHub/GenHub/Features/GameProfiles/ViewModels/AddLocalContentViewModel.cs, GenHub/GenHub/Features/GameProfiles/ViewModels/FileTreeItem.cs, GenHub/GenHub/Features/GameProfiles/Views/AddLocalContentWindow.axaml, GenHub/GenHub/Features/GameProfiles/Views/AddLocalContentWindow.axaml.cs
New Add Local Content view model, file-tree model, and window with staging, zip handling, preview, drag/drop and commands.
Settings VM messaging & XAML changes
GenHub/GenHub/Features/GameProfiles/ViewModels/GameProfileSettingsViewModel.cs, GenHub/GenHub/Features/GameProfiles/Views/GameProfileSettingsWindow.axaml
Settings VM now implements IRecipient<ContentAcquiredMessage> and registers for messages; embedded Add Local Content dialog removed from settings XAML.
Profile launcher dependency validation
GenHub/GenHub/Features/GameProfiles/Services/ProfileLauncherFacade.cs
Optional dependencies no longer produce errors; logs include Optional flag.
MainViewModel refactor & tests
GenHub/GenHub/Common/ViewModels/MainViewModel.cs, tests GenHub/GenHub.Tests/.../MainViewModelTests.cs
Reworked to use fewer private fields, injected services, added SelectedTab observable; tests updated to match constructor.
Workspace / User data path helpers
GenHub/GenHub/Features/UserData/Services/UserDataTrackerService.cs, GenHub/GenHub/Features/Workspace/Strategies/WorkspaceStrategyBase.cs
Added StripLeadingDirectory helper and use it when resolving map/replay/screenshot targets.
Local content allowed types
GenHub/GenHub.Core/Services/Content/LocalContentService.cs
Added ContentType.Mod to allowed local content types.
Map & replay constants, fixes & UI
GenHub/GenHub.Core/Constants/MapManagerConstants.cs, GenHub/GenHub.Core/Constants/ReplayManagerConstants.cs, GenHub/GenHub/Features/Tools/MapManager/Services/MapImportService.cs, GenHub/GenHub/Infrastructure/Imaging/TgaImageParser.cs, converters, views
New constants for display/extensions/retention, path-separator fix, defensive TGA bounds checks, converters and UI updated to use constants.
Directory service rename & cross-platform behavior
GenHub/GenHub.Core/Interfaces/Tools/MapManager/IMapDirectoryService.cs, GenHub/GenHub.Core/Interfaces/Tools/ReplayManager/IReplayDirectoryService.cs, implementations and call sites
Renamed OpenInExplorerOpenDirectory and RevealInExplorerRevealFile; implementations updated for cross-platform behavior.
CI & secrets
.github/scripts/inject-token.ps1, .github/workflows/ci.yml
When UPLOADTHING_TOKEN missing allow fork PR builds by assigning a dummy token; removed Inject Secrets step for Linux build path.
Tests adjusted for API changes
GenHub/GenHub.Tests/...
Updated mocks and call sites for new AddManifestAsync progress parameter; other test adjustments (removed mocks, provider swaps).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant GitHubDeliverer as GitHub Deliverer
    participant Downloader as File Downloader
    participant Extractor as ZIP Extractor
    participant ManifestPool as Manifest Pool
    participant CAS as CAS Storage
    participant Progress as Progress Reporter

    rect rgb(240,248,255)
    Client->>GitHubDeliverer: DeliverContentAsync(manifest, progress)
    GitHubDeliverer->>Downloader: Download files (per-file)
    Downloader->>Progress: Report Downloading (index/total, speed)
    end

    rect rgb(230,255,230)
    GitHubDeliverer->>Extractor: Open ZIP & extract entries
    Extractor->>Progress: Report Extraction (mapped percent range)
    end

    rect rgb(255,250,230)
    GitHubDeliverer->>ManifestPool: AddManifestAsync(manifest, manifestDir, progress: storageProgress)
    ManifestPool->>CAS: StoreContentAsync(files, progress: storageProgress)
    CAS->>Progress: Report StoringInCas (percent, fileIndex)
    ManifestPool-->>GitHubDeliverer: Store result
    end

    GitHubDeliverer-->>Client: Delivery complete (result)
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly Related PRs

Poem

🐰 I hopped through manifests and zip-filled nights,
I tracked each byte and lit the progress lights,
I painted themes in green and dusk and gold,
I opened windows where local maps unfold.
A nibble, a hop—content arrives, bright and bold!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.73% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The pull request title "Feat/map manager" is vague and generic, using non-descriptive formatting that does not clearly convey the primary changes in the changeset. Revise the title to be more specific and descriptive of the main changes, such as 'Add local content import and map manager improvements' or similar, rather than using generic feature branch naming.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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 and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
GenHub/GenHub/Features/Downloads/ViewModels/PublisherCardViewModel.cs (1)

622-639: Bug: Logging uses wrong variable in foreach loop.

The logging statements on lines 627-637 reference manifest.Id (the outer variable from line 572) instead of m (the loop variable). This will log the same manifest ID for all iterations instead of each variant's ID.

🔎 Proposed fix
                         foreach (var m in justInstalledGameClients)
                         {
                             var profileResult = await _profileService.CreateProfileFromManifestAsync(m);
                             if (profileResult.Success)
                             {
                                 _logger.LogInformation(
                                     "Created profile for {ManifestId}: {ProfileName}",
-                                    manifest.Id,
+                                    m.Id,
                                     profileResult.Data?.Name);
                             }
                             else
                             {
                                 _logger.LogWarning(
                                     "Failed to create profile for {ManifestId}: {Errors}",
-                                    manifest.Id,
+                                    m.Id,
                                     string.Join(", ", profileResult.Errors));
                             }
                         }
🧹 Nitpick comments (23)
GenHub/GenHub/Features/GameProfiles/ViewModels/FileTreeItem.cs (1)

28-28: Consider using explicit ObservableCollection initialization for better compatibility.

The collection expression [] syntax may not work reliably with ObservableCollection<T> depending on the C# compiler version. The traditional initialization is more explicit and guaranteed to work.

🔎 Proposed fix
-    public ObservableCollection<FileTreeItem> Children { get; set; } = [];
+    public ObservableCollection<FileTreeItem> Children { get; set; } = new ObservableCollection<FileTreeItem>();

Based on relevant code snippets provided.

GenHub/GenHub/Features/UserData/Services/UserDataTrackerService.cs (1)

712-731: Extract duplicated helper to a shared utility class.

This method is duplicated in WorkspaceStrategyBase.cs (lines 632-644). Consider extracting it to a shared utility class to follow the DRY principle and ensure consistent behavior across the codebase.

Optional: Address minor return value inconsistency

When the prefix doesn't match, the method returns the original path rather than the normalized version. This could result in inconsistent path separators (mixed forward/back slashes) in the returned values. Consider returning the normalized path in both cases for consistency:

 private static string StripLeadingDirectory(string path, string directoryName)
 {
     // Handle both forward and back slashes
     var normalized = path.Replace('\\', '/');
     var prefix = directoryName + "/";

     if (normalized.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
     {
         return normalized[prefix.Length..];
     }

-    return path;
+    return normalized;
 }
GenHub/GenHub/Features/Content/Services/Helpers/GitHubInferenceHelper.cs (1)

53-61: Logic is correct; consider aligning with InferGameTypeFromAsset for consistency.

The priority-based approach works correctly: zero hour indicators are checked first with early return, so the generals check at line 60 only executes when no zero hour indicators are present. This simplifies the previous implementation by eliminating redundant exclusion checks.

However, InferGameTypeFromAsset (lines 156-159) uses an explicit exclusion approach for the generals check. For maintainability, consider aligning both methods to use the same pattern—either priority-based (current) or explicit exclusion (like InferGameTypeFromAsset).

Optional: Align with explicit exclusion pattern

If you prefer consistency with InferGameTypeFromAsset:

 // Check for Generals indicators
-if (searchText.Contains("generals", StringComparison.OrdinalIgnoreCase))
+if (searchText.Contains("generals", StringComparison.OrdinalIgnoreCase) &&
+    !searchText.Contains("zero hour", StringComparison.OrdinalIgnoreCase) &&
+    !searchText.Contains("zh", StringComparison.OrdinalIgnoreCase) &&
+    !searchText.Contains("zerohour", StringComparison.OrdinalIgnoreCase))
     return (Type: GameType.Generals, IsInferred: true);

Note: This is purely stylistic; the current implementation is functionally correct.

GenHub/GenHub/Common/ViewModels/MainViewModel.cs (2)

90-108: Consider documenting the Tools tab exclusion logic.

The redirect from NavigationTab.Tools to GameProfiles on initial load is intentional but not explained. A brief comment would clarify the reasoning for future maintainers.

🔎 Suggested documentation
     private static NavigationTab LoadInitialTab(IConfigurationProviderService configurationProvider, ILogger<MainViewModel>? logger)
     {
         try
         {
             var tab = configurationProvider.GetLastSelectedTab();
+            // Tools tab should not be restored on startup (e.g., requires explicit user navigation)
             if (tab == NavigationTab.Tools)
             {
                 tab = NavigationTab.GameProfiles;
             }

277-293: Fire-and-forget on SaveAsync may lose errors silently.

Line 286 discards the Task from SaveAsync(), meaning any async exceptions will be unobserved. For a low-priority preference like tab selection, this is acceptable, but consider logging failures if save reliability becomes important.

🔎 Optional: Add async error handling
-            _ = userSettingsService.SaveAsync();
+            _ = userSettingsService.SaveAsync().ContinueWith(
+                t => logger?.LogWarning(t.Exception, "Failed to persist tab selection"),
+                TaskContinuationOptions.OnlyOnFaulted);
GenHub/GenHub/Features/Workspace/Strategies/WorkspaceStrategyBase.cs (1)

629-645: Consider consistent path separator handling.

The method returns paths with forward slashes when the prefix is stripped (line 641) but returns the original path with potentially mixed separators when the prefix doesn't match (line 644). While Path.Combine handles both separators on Windows, consistent behavior would be clearer.

🔎 Suggested fix for consistent path separators
 private static string StripLeadingDirectory(string path, string directoryName)
 {
     // Handle both forward and back slashes
     var normalized = path.Replace('\\', '/');
     var prefix = directoryName + "/";
 
     if (normalized.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
     {
         return normalized[prefix.Length..];
     }
 
-    return path;
+    return normalized;
 }
GenHub/GenHub/Features/Downloads/Views/PublisherCardView.axaml (1)

209-209: Consider binding the progress bar color to the theme color from metadata.

This PR introduces ThemeColor metadata for content theming, but the progress bar still uses a hardcoded color. For consistency with the theming approach, consider binding the Foreground property to the ThemeColor from the content item's metadata (if available), with the current color as a fallback.

💡 Suggested approach

You could create a converter that extracts ThemeColor from the ContentItem's metadata and use it like:

 <ProgressBar Value="{Binding DownloadProgress}"
              Maximum="100"
              Height="4"
              IsVisible="{Binding IsDownloading}"
-             Foreground="#A15DF3"
+             Foreground="{Binding ThemeColor, FallbackValue='#A15DF3'}"
              Background="#333333" />

This would make the progress bar color dynamic based on the publisher's theme color defined in constants like CommunityOutpostConstants.ThemeColor.

GenHub/GenHub/Features/Content/Services/CommunityOutpost/CommunityOutpostDeliverer.cs (1)

273-277: Consider wiring up storage progress reporting.

The PR introduces storage progress reporting (ContentStorageProgress), but this call passes null for the progress parameter. Since DeliverContentAsync already receives an IProgress<ContentAcquisitionProgress> parameter, consider creating an adapter to forward storage progress to the caller. This would provide better visibility during the "Registering manifests to content library" phase.

💡 Example approach
+// Create progress adapter to map storage progress to acquisition progress
+IProgress<ContentStorageProgress>? storageProgress = null;
+if (progress != null)
+{
+    storageProgress = new Progress<ContentStorageProgress>(sp =>
+    {
+        progress.Report(new ContentAcquisitionProgress
+        {
+            Phase = ContentAcquisitionPhase.Copying,
+            ProgressPercentage = 70 + (int)(sp.ProgressPercentage * 0.2),
+            CurrentOperation = sp.CurrentOperation ?? "Storing content",
+        });
+    });
+}
+
 var addResult = await manifestPool.AddManifestAsync(
     manifest,
     extractPath,
-    null,
+    storageProgress,
     cancellationToken);
GenHub/GenHub/Features/Content/Services/ContentOrchestrator.cs (1)

531-531: Consider forwarding storage progress for better user feedback.

Similar to other content deliverers, this call doesn't forward progress during manifest storage. Since the orchestrator already has a progress parameter and is at the 80% mark (extracting phase), forwarding storage progress would provide users with visibility into the "Adding to content library" operation.

💡 Suggested approach
+// Forward storage progress to acquisition progress
+IProgress<ContentStorageProgress>? storageProgress = null;
+if (progress != null)
+{
+    storageProgress = new Progress<ContentStorageProgress>(sp =>
+    {
+        var pct = ContentConstants.ProgressStepExtracting + (int)(sp.ProgressPercentage * 0.15);
+        progress.Report(new ContentAcquisitionProgress
+        {
+            Phase = ContentAcquisitionPhase.Extracting,
+            ProgressPercentage = pct,
+            CurrentOperation = sp.CurrentOperation ?? "Storing in library",
+        });
+    });
+}
+
-await _manifestPool.AddManifestAsync(prepareResult.Data, stagingDir, cancellationToken: cancellationToken);
+await _manifestPool.AddManifestAsync(prepareResult.Data, stagingDir, storageProgress, cancellationToken);
GenHub/GenHub/Features/GameProfiles/ViewModels/GameProfileLauncherViewModel.ManifestHelpers.cs (1)

125-125: Consider wiring progress reporting for better UX.

The call correctly adapts to the new AddManifestAsync signature by passing null for progress. However, since this is a user-facing manual installation flow, reporting progress could improve the experience when registering large installations.

If this ViewModel has access to a progress reporter (e.g., via a property or parameter), consider passing it through instead of null.

GenHub/GenHub/Features/Manifest/ManifestProvider.cs (1)

96-96: LGTM! Progress parameter consistently set to null.

All four AddManifestAsync call sites correctly pass null for the new progress parameter. This is acceptable for ManifestProvider operations, which are typically fast internal operations.

For future enhancement, consider whether progress reporting would be beneficial for long-running manifest addition operations, particularly when adding large generated manifests with many files.

Also applies to: 154-154, 208-208, 264-264

GenHub/GenHub/Features/Content/Services/Publishers/GitHubManifestFactory.cs (2)

61-87: Verify hash computation cancellation and error handling

The loop checks cancellationToken.IsCancellationRequested at Line 63 but then calls ComputeFileHashAsync at Line 70 without ensuring that exceptions from file I/O or hash computation are handled gracefully. Consider wrapping the file processing in a try-catch block to handle scenarios where file access fails (e.g., locked files, permission issues).

Additionally, the IsRequired = true assignment at Line 82 marks all files as required. This may not be appropriate for all files in extracted archives—consider whether some files should be optional.

🔎 Suggested error handling enhancement
 foreach (var filePath in allFiles)
 {
     if (cancellationToken.IsCancellationRequested)
         break;
 
-    var relativePath = Path.GetRelativePath(extractedDirectory, filePath);
-    var fileInfo = new FileInfo(filePath);
-
-    // Compute hash for ContentAddressable storage
-    string fileHash = await hashProvider.ComputeFileHashAsync(filePath, cancellationToken);
-
-    // Determine if executable (simple heuristic for now, can be improved)
-    bool isExecutable = filePath.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) ||
-                        filePath.EndsWith(".bat", StringComparison.OrdinalIgnoreCase) ||
-                        filePath.EndsWith(".cmd", StringComparison.OrdinalIgnoreCase);
-
-    files.Add(new ManifestFile
-    {
-        RelativePath = relativePath,
-        Size = fileInfo.Length,
-        Hash = fileHash,
-        IsRequired = true,
-        IsExecutable = isExecutable,
-        SourceType = ContentSourceType.ContentAddressable,
-        SourcePath = filePath,
-    });
+    try
+    {
+        var relativePath = Path.GetRelativePath(extractedDirectory, filePath);
+        var fileInfo = new FileInfo(filePath);
+
+        // Compute hash for ContentAddressable storage
+        string fileHash = await hashProvider.ComputeFileHashAsync(filePath, cancellationToken);
+
+        // Determine if executable (simple heuristic for now, can be improved)
+        bool isExecutable = filePath.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) ||
+                            filePath.EndsWith(".bat", StringComparison.OrdinalIgnoreCase) ||
+                            filePath.EndsWith(".cmd", StringComparison.OrdinalIgnoreCase);
+
+        files.Add(new ManifestFile
+        {
+            RelativePath = relativePath,
+            Size = fileInfo.Length,
+            Hash = fileHash,
+            IsRequired = true,
+            IsExecutable = isExecutable,
+            SourceType = ContentSourceType.ContentAddressable,
+            SourcePath = filePath,
+        });
+    }
+    catch (Exception ex)
+    {
+        logger.LogWarning(ex, "Failed to process file {FilePath}, skipping", filePath);
+        // Continue processing other files
+    }
 }

73-75: Consider cross-platform executability detection

The executability heuristic only checks for Windows file extensions (.exe, .bat, .cmd). On Unix-based systems, executables may not have extensions and rely on file permissions. While this may be acceptable for the current use case (C&C Generals runs primarily on Windows), consider documenting this limitation or enhancing the detection logic for broader cross-platform support.

GenHub/GenHub/Features/Content/Services/GitHub/GitHubContentDeliverer.cs (1)

141-203: Verify manual ZIP extraction necessity and add error handling

The code replaces the simpler ZipFile.ExtractToDirectory with manual entry-by-entry extraction (Lines 152-183). While this enables per-file progress reporting, it lacks error handling for individual entry extraction failures. If an entry fails to extract (e.g., due to I/O errors or path issues), the entire operation will fail rather than logging and continuing.

Additionally, verify that the progress range alignment is correct: downloads end at 65%, extraction runs 65-70%, leaving 70-90% unaccounted for before storage at 90%.

🔎 Suggested error handling enhancement
 foreach (var entry in archive.Entries)
 {
     if (string.IsNullOrEmpty(entry.Name)) continue; // Skip directories
 
-    var destinationPath = Path.Combine(targetDirectory, entry.FullName);
-    var destinationDir = Path.GetDirectoryName(destinationPath);
-    if (!string.IsNullOrEmpty(destinationDir)) Directory.CreateDirectory(destinationDir);
-
-    entry.ExtractToFile(destinationPath, overwrite: true);
-    currentEntry++;
+    try
+    {
+        var destinationPath = Path.Combine(targetDirectory, entry.FullName);
+        var destinationDir = Path.GetDirectoryName(destinationPath);
+        if (!string.IsNullOrEmpty(destinationDir)) Directory.CreateDirectory(destinationDir);
+
+        entry.ExtractToFile(destinationPath, overwrite: true);
+        currentEntry++;
+    }
+    catch (Exception ex)
+    {
+        logger.LogWarning(ex, "Failed to extract entry {EntryName}, skipping", entry.FullName);
+        continue;
+    }
 
     // Map extraction progress from 65% to 70%
     double extractStart = 65;
GenHub/GenHub/Features/GameProfiles/ViewModels/GameProfileSettingsViewModel.cs (2)

99-101: Consider unregistering from messages to prevent memory leaks.

The ViewModel registers with WeakReferenceMessenger.Default.Register(this) but there's no corresponding unregistration when the ViewModel is disposed or the window closes. While WeakReferenceMessenger uses weak references, explicit unregistration is a best practice, especially if the ViewModel outlives expectations.

🔎 Proposed fix

Consider implementing IDisposable or adding cleanup logic:

// Add a cleanup method or implement IDisposable
public void Cleanup()
{
    WeakReferenceMessenger.Default.Unregister<Core.Models.Content.ContentAcquiredMessage>(this);
}

504-512: Fire-and-forget async call should handle exceptions.

The Receive method discards the task from LoadAvailableContentAsync() using _ =. If an exception occurs during content loading, it will be unobserved. Consider wrapping in a try-catch or using a helper method.

🔎 Proposed improvement
 public void Receive(Core.Models.Content.ContentAcquiredMessage message)
 {
     // Refresh available content when new content is acquired
-    _ = LoadAvailableContentAsync();
+    _ = SafeLoadAvailableContentAsync();
+}
+
+private async Task SafeLoadAvailableContentAsync()
+{
+    try
+    {
+        await LoadAvailableContentAsync();
+    }
+    catch (Exception ex)
+    {
+        logger?.LogError(ex, "Error refreshing content after acquisition");
+    }
 }
GenHub/GenHub/Features/GameProfiles/Views/AddLocalContentWindow.axaml.cs (2)

27-66: Event subscription without cleanup may cause memory leak.

The RequestClose event subscription on line 32 is never unsubscribed. While this may not be critical if the window and ViewModel are disposed together, it's safer to unsubscribe when the window is closed.

🔎 Proposed fix
 protected override void OnDataContextChanged(EventArgs e)
 {
     base.OnDataContextChanged(e);
     if (DataContext is AddLocalContentViewModel vm)
     {
-        vm.RequestClose += (s, result) => Close(result);
+        vm.RequestClose += OnRequestClose;

         // Wire up the browse delegates
         vm.BrowseFolderAction = async () =>
         {
             // ... existing code
         };
     }
 }
+
+private void OnRequestClose(object? sender, bool result)
+{
+    if (DataContext is AddLocalContentViewModel vm)
+    {
+        vm.RequestClose -= OnRequestClose;
+    }
+    Close(result);
+}

86-101: Consider adding error handling for robustness.

While ImportContentAsync has internal exception handling, wrapping the loop in a try-catch would provide defense-in-depth against unexpected exceptions during file enumeration.

🔎 Proposed fix
 private async void OnDrop(object? sender, DragEventArgs e)
 {
     if (DataContext is not AddLocalContentViewModel vm) return;

-    var files = e.Data.GetFiles();
-    if (files != null)
+    try
     {
-        foreach (var file in files)
+        var files = e.Data.GetFiles();
+        if (files != null)
         {
-            if (file?.Path?.LocalPath is { } path)
+            foreach (var file in files)
             {
-                await vm.ImportContentAsync(path);
+                if (file?.Path?.LocalPath is { } path)
+                {
+                    await vm.ImportContentAsync(path);
+                }
             }
         }
     }
+    catch (Exception ex)
+    {
+        // Log or handle unexpected errors during drop operation
+        System.Diagnostics.Debug.WriteLine($"Drop error: {ex.Message}");
+    }
 }
GenHub/GenHub/Features/GameProfiles/Views/AddLocalContentWindow.axaml (2)

82-89: Use static reference for type-safe ContentType comparison.

The ConverterParameter=GameClient is a string comparison which could silently fail if the enum value name changes. Use a static reference for type safety, consistent with other bindings in this file.

🔎 Proposed fix
 <!-- Target Game -->
-<StackPanel Spacing="8" IsVisible="{Binding SelectedContentType, Converter={x:Static ObjectConverters.Equal}, ConverterParameter=GameClient}">
+<StackPanel Spacing="8" IsVisible="{Binding SelectedContentType, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static models:ContentType.GameClient}}">
     <TextBlock Text="TARGET GAME" Classes="label"/>

Note: You'll need to add the models namespace if not already present:

xmlns:models="clr-namespace:GenHub.Core.Models.Enums;assembly=GenHub.Core"

156-160: Consider using a clearer empty-state binding.

The !FileTree.Count negation syntax works but relies on implicit int-to-bool conversion. For improved readability, consider using a converter or an IsEmpty property on the ViewModel.

🔎 Alternative approaches

Option 1 - Use existing converter:

IsVisible="{Binding FileTree.Count, Converter={x:Static ObjectConverters.Equal}, ConverterParameter=0}"

Option 2 - Add a property to the ViewModel:

public bool IsFileTreeEmpty => !FileTree.Any();
GenHub/GenHub/Features/GameProfiles/ViewModels/AddLocalContentViewModel.cs (3)

213-234: Truncation limits may confuse users with large imports.

The Take(20) and Take(50) limits silently truncate the preview tree without user notification. Users importing large content packs may not realize files are hidden.

🔎 Proposed improvement

Consider adding a visual indicator when items are truncated:

 foreach (var d in dir.GetDirectories().Take(20))
 {
     items.Add(new FileTreeItem { /* ... */ });
 }
+
+var remainingDirs = dir.GetDirectories().Length - items.Count(i => !i.IsFile);
+if (remainingDirs > 0)
+{
+    items.Add(new FileTreeItem { Name = $"... and {remainingDirs} more folders", IsFile = false });
+}

 foreach (var f in dir.GetFiles().Take(50))
 {
     items.Add(new FileTreeItem { Name = f.Name, IsFile = true, FullPath = f.FullName });
 }
+
+var remainingFiles = dir.GetFiles().Length - items.Count(i => i.IsFile);
+if (remainingFiles > 0)
+{
+    items.Add(new FileTreeItem { Name = $"... and {remainingFiles} more files", IsFile = true });
+}

398-411: Empty catch block swallows cleanup errors silently.

Even for non-critical cleanup, logging the error aids debugging when temp directories accumulate.

🔎 Proposed fix
 private void CleanupStaging()
 {
     try
     {
         if (Directory.Exists(_stagingPath))
         {
             Directory.Delete(_stagingPath, true);
         }
     }
-    catch
+    catch (Exception ex)
     {
-        // Ignore cleanup errors
+        logger?.LogWarning(ex, "Failed to cleanup staging directory: {Path}", _stagingPath);
     }
 }

278-316: Synchronous file operations could freeze UI for large directories.

File.Delete and Directory.Delete at lines 295-299 are blocking operations. Consider wrapping them in Task.Run like other file operations in this class.

🔎 Proposed fix
-if (item.IsFile && File.Exists(item.FullPath))
-{
-    File.Delete(item.FullPath);
-}
-else if (!item.IsFile && Directory.Exists(item.FullPath))
-{
-    Directory.Delete(item.FullPath, true);
-}
+await Task.Run(() =>
+{
+    if (item.IsFile && File.Exists(item.FullPath))
+    {
+        File.Delete(item.FullPath);
+    }
+    else if (!item.IsFile && Directory.Exists(item.FullPath))
+    {
+        Directory.Delete(item.FullPath, true);
+    }
+});
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1733c9e and 9123c1c.

📒 Files selected for processing (41)
  • GenHub/GenHub.Core/Constants/CommunityOutpostConstants.cs
  • GenHub/GenHub.Core/Constants/ContentConstants.cs
  • GenHub/GenHub.Core/Constants/GeneralsOnlineConstants.cs
  • GenHub/GenHub.Core/Constants/SuperHackersConstants.cs
  • GenHub/GenHub.Core/Interfaces/GameProfiles/IGameClientProfileService.cs
  • GenHub/GenHub.Core/Interfaces/Manifest/IContentManifestPool.cs
  • GenHub/GenHub.Core/Models/Content/ContentAcquiredMessage.cs
  • GenHub/GenHub.Core/Models/Content/ContentAcquisitionPhase.cs
  • GenHub/GenHub.Core/Models/Enums/ContentType.cs
  • GenHub/GenHub.Core/Models/Manifest/ContentMetadata.cs
  • GenHub/GenHub.Core/Services/Content/LocalContentService.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Content/ContentOrchestratorTests.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameClients/GameClientDetectorTests.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameClients/GameClientManifestIntegrationTests.cs
  • GenHub/GenHub/Common/ViewModels/MainViewModel.cs
  • GenHub/GenHub/Features/Content/Services/CommunityOutpost/CommunityOutpostDeliverer.cs
  • GenHub/GenHub/Features/Content/Services/CommunityOutpost/CommunityOutpostManifestFactory.cs
  • GenHub/GenHub/Features/Content/Services/ContentOrchestrator.cs
  • GenHub/GenHub/Features/Content/Services/GeneralsOnline/GeneralsOnlineDeliverer.cs
  • GenHub/GenHub/Features/Content/Services/GeneralsOnline/GeneralsOnlineManifestFactory.cs
  • GenHub/GenHub/Features/Content/Services/GitHub/GitHubContentDeliverer.cs
  • GenHub/GenHub/Features/Content/Services/Helpers/GitHubInferenceHelper.cs
  • GenHub/GenHub/Features/Content/Services/Publishers/GitHubManifestFactory.cs
  • GenHub/GenHub/Features/Content/Services/Publishers/SuperHackersManifestFactory.cs
  • GenHub/GenHub/Features/Downloads/ViewModels/PublisherCardViewModel.cs
  • GenHub/GenHub/Features/Downloads/Views/PublisherCardView.axaml
  • GenHub/GenHub/Features/GameInstallations/GameInstallationService.cs
  • GenHub/GenHub/Features/GameProfiles/Services/GameClientProfileService.cs
  • GenHub/GenHub/Features/GameProfiles/Services/ProfileLauncherFacade.cs
  • GenHub/GenHub/Features/GameProfiles/ViewModels/AddLocalContentViewModel.cs
  • GenHub/GenHub/Features/GameProfiles/ViewModels/FileTreeItem.cs
  • GenHub/GenHub/Features/GameProfiles/ViewModels/GameProfileLauncherViewModel.ManifestHelpers.cs
  • GenHub/GenHub/Features/GameProfiles/ViewModels/GameProfileSettingsViewModel.cs
  • GenHub/GenHub/Features/GameProfiles/Views/AddLocalContentWindow.axaml
  • GenHub/GenHub/Features/GameProfiles/Views/AddLocalContentWindow.axaml.cs
  • GenHub/GenHub/Features/GameProfiles/Views/GameProfileSettingsWindow.axaml
  • GenHub/GenHub/Features/Manifest/ContentManifestPool.cs
  • GenHub/GenHub/Features/Manifest/ManifestProvider.cs
  • GenHub/GenHub/Features/UserData/Services/UserDataTrackerService.cs
  • GenHub/GenHub/Features/Workspace/Strategies/WorkspaceStrategyBase.cs
  • GenHub/GenHub/Infrastructure/DependencyInjection/ContentPipelineModule.cs
🧰 Additional context used
🧬 Code graph analysis (10)
GenHub/GenHub/Features/Content/Services/Publishers/SuperHackersManifestFactory.cs (1)
GenHub/GenHub.Core/Constants/SuperHackersConstants.cs (1)
  • SuperHackersConstants (6-108)
GenHub/GenHub/Features/UserData/Services/UserDataTrackerService.cs (4)
GenHub/GenHub/Features/Content/Services/CommunityOutpost/CommunityOutpostManifestFactory.cs (1)
  • ContentInstallTarget (156-198)
GenHub/GenHub/Features/Manifest/ContentManifestBuilder.cs (1)
  • ContentInstallTarget (753-782)
GenHub/GenHub.Core/Constants/GameSettingsConstants.cs (2)
  • GameSettingsConstants (6-181)
  • FolderNames (110-141)
GenHub/GenHub/Features/Workspace/Strategies/WorkspaceStrategyBase.cs (1)
  • StripLeadingDirectory (633-645)
GenHub/GenHub/Features/Content/Services/CommunityOutpost/CommunityOutpostManifestFactory.cs (1)
GenHub/GenHub.Core/Constants/CommunityOutpostConstants.cs (1)
  • CommunityOutpostConstants (11-102)
GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameClients/GameClientManifestIntegrationTests.cs (1)
GenHub/GenHub.Core/Models/Manifest/ContentManifest.cs (1)
  • ContentManifest (10-53)
GenHub/GenHub/Features/Downloads/ViewModels/PublisherCardViewModel.cs (2)
GenHub/GenHub/Features/Manifest/ContentManifestBuilder.cs (1)
  • ContentManifest (693-724)
GenHub/GenHub.Core/Models/Manifest/ContentManifest.cs (1)
  • ContentManifest (10-53)
GenHub/GenHub/Features/Content/Services/Publishers/GitHubManifestFactory.cs (5)
GenHub/GenHub/Features/Content/Services/Publishers/PublisherManifestFactoryResolver.cs (1)
  • IPublisherManifestFactory (19-41)
GenHub/GenHub/Features/Content/Services/Publishers/SuperHackersManifestFactory.cs (2)
  • CanHandle (46-55)
  • GetManifestDirectory (100-112)
GenHub/GenHub.Core/Interfaces/GameProfiles/IGameClientProfileService.cs (4)
  • Task (26-32)
  • Task (46-52)
  • Task (61-63)
  • Task (71-73)
GenHub/GenHub.Core/Interfaces/Manifest/IContentManifestPool.cs (8)
  • Task (20-20)
  • Task (30-30)
  • Task (38-38)
  • Task (45-45)
  • Task (53-53)
  • Task (61-61)
  • Task (69-69)
  • Task (77-77)
GenHub/GenHub.Core/Constants/PublisherInfoConstants.cs (1)
  • Name (121-133)
GenHub/GenHub/Features/Content/Services/GeneralsOnline/GeneralsOnlineManifestFactory.cs (1)
GenHub/GenHub.Core/Constants/GeneralsOnlineConstants.cs (1)
  • GeneralsOnlineConstants (6-125)
GenHub/GenHub/Features/GameProfiles/Services/ProfileLauncherFacade.cs (1)
GenHub/GenHub.Core/Constants/PublisherInfoConstants.cs (1)
  • Name (121-133)
GenHub/GenHub/Features/GameProfiles/Services/GameClientProfileService.cs (8)
GenHub/GenHub/Features/GameProfiles/ViewModels/GameProfileLauncherViewModel.cs (1)
  • GetThemeColorForGameType (310-313)
GenHub/GenHub/Features/Content/Services/Helpers/GitHubInferenceHelper.cs (2)
  • GameType (49-64)
  • GameType (135-162)
GenHub/GenHub.Core/Models/GameClients/GameClient.cs (1)
  • GameClient (8-107)
GenHub/GenHub.Core/Constants/PublisherTypeConstants.cs (1)
  • PublisherTypeConstants (21-59)
GenHub/GenHub.Core/Constants/SuperHackersConstants.cs (1)
  • SuperHackersConstants (6-108)
GenHub/GenHub.Core/Constants/PublisherInfoConstants.cs (1)
  • GeneralsOnline (104-114)
GenHub/GenHub.Core/Constants/GeneralsOnlineConstants.cs (1)
  • GeneralsOnlineConstants (6-125)
GenHub/GenHub.Core/Constants/CommunityOutpostConstants.cs (1)
  • CommunityOutpostConstants (11-102)
GenHub/GenHub/Features/GameProfiles/ViewModels/AddLocalContentViewModel.cs (3)
GenHub/GenHub/Features/GameProfiles/ViewModels/FileTreeItem.cs (1)
  • FileTreeItem (8-29)
GenHub/GenHub.Core/Interfaces/Manifest/IContentManifestPool.cs (8)
  • Task (20-20)
  • Task (30-30)
  • Task (38-38)
  • Task (45-45)
  • Task (53-53)
  • Task (61-61)
  • Task (69-69)
  • Task (77-77)
GenHub/GenHub.Core/Services/Content/LocalContentService.cs (1)
  • Task (41-131)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build Linux
  • GitHub Check: Build Windows
🔇 Additional comments (48)
GenHub/GenHub/Features/UserData/Services/UserDataTrackerService.cs (1)

705-707: LGTM! Path normalization prevents double-nesting.

The use of StripLeadingDirectory correctly prevents double-nesting of folder names (e.g., "Maps/Maps/file.map") when combining relative paths with the base directory that already includes the target folder name.

GenHub/GenHub/Common/ViewModels/MainViewModel.cs (3)

45-88: Well-structured primary constructor with DI.

The refactor to C# 12 primary constructor syntax is clean and properly encapsulates dependencies. Field initialization via LoadInitialTab is safe since the parameters are guaranteed non-null at that point.


171-180: LGTM!

The logger usage is consistent with the primary constructor pattern, and the fire-and-forget for background updates is appropriate since CheckForUpdatesInBackgroundAsync handles its own exceptions.


195-275: LGTM!

The dependency reference updates are consistent with the primary constructor pattern. The dual exception handling (in CheckForUpdatesAsync and its wrapper) provides robust error isolation for background operations.

GenHub/GenHub.Core/Models/Content/ContentAcquisitionPhase.cs (1)

43-46: LGTM! Clear and well-documented enum addition.

The new StoringInCas phase is logically placed in the acquisition workflow and has clear documentation. This aligns well with the progress reporting enhancements mentioned in the summary.

GenHub/GenHub/Features/Workspace/Strategies/WorkspaceStrategyBase.cs (1)

475-477: LGTM! Path resolution correctly prevents directory duplication.

The updated logic properly strips leading directory names (Maps, Replays, Screenshots) from relative paths before combining with the target base path, preventing paths like Maps/Maps/mymap.map.

GenHub/GenHub.Core/Constants/SuperHackersConstants.cs (1)

38-46: LGTM! Theme color constants are well-defined.

The new theme color constants are properly documented and logically placed with other UI metadata. The color values (dark red for Zero Hour, orange for Generals) provide clear visual differentiation between the variants.

GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Content/ContentOrchestratorTests.cs (2)

114-114: LGTM! Test mock correctly updated for new signature.

The mock setup properly includes the new IProgress<ContentStorageProgress> parameter added to AddManifestAsync, ensuring the test reflects the updated interface.


132-132: LGTM! Verification correctly validates the new signature.

The verification properly checks that AddManifestAsync is called with all required parameters including the new progress reporter.

GenHub/GenHub/Infrastructure/DependencyInjection/ContentPipelineModule.cs (1)

154-157: Multiple IPublisherManifestFactory registrations are correctly handled.

The PublisherManifestFactoryResolver (line 22) uses FirstOrDefault(f => f.CanHandle(manifest)) to select the appropriate factory. Both SuperHackersManifestFactory and GitHubManifestFactory implement distinct CanHandle logic: SuperHackersManifestFactory requires the "thesuperhackers" publisher type AND GameClient content type (line 54), while GitHubManifestFactory requires only the "github" publisher type (line 30). No conflicts exist between these registrations.

GenHub/GenHub.Core/Constants/CommunityOutpostConstants.cs (1)

38-41: LGTM!

The ThemeColor constant is properly documented and follows the same pattern used in other publisher constants. The dark green color (#2D5A27) appears appropriate for Community Outpost branding.

GenHub/GenHub.Core/Models/Manifest/ContentMetadata.cs (1)

42-46: LGTM!

The ThemeColor property is a clean addition that extends the metadata model for theming support. The nullable string type is appropriate since theme colors are optional, and the XML documentation is clear.

GenHub/GenHub/Features/Content/Services/GeneralsOnline/GeneralsOnlineDeliverer.cs (1)

139-139: LGTM! Named parameter improves code clarity.

The refactoring to use named parameter binding (cancellationToken: cancellationToken) makes the code more explicit and readable, especially when optional parameters are involved. The change is functionally equivalent and consistently applied across all three manifest registrations.

Also applies to: 154-154, 169-169

GenHub/GenHub/Features/Content/Services/CommunityOutpost/CommunityOutpostManifestFactory.cs (1)

277-277: LGTM! Theme color properly integrated.

The addition of ThemeColor to the manifest metadata follows the established pattern used by other publishers and enhances the UI theming capabilities. The constant is correctly sourced from CommunityOutpostConstants.ThemeColor.

GenHub/GenHub.Core/Constants/ContentConstants.cs (1)

83-86: LGTM! Storage progress step properly added.

The new ProgressStepStoring constant (90%) logically fits between the extracting (85%) and completion (100%) phases, providing better granularity for content acquisition progress reporting.

GenHub/GenHub.Core/Constants/GeneralsOnlineConstants.cs (1)

39-42: LGTM! Theme color constant properly defined.

The addition of ThemeColor follows the consistent pattern established across other publisher constants and uses a valid hex color code format. This enhances the theming support for Generals Online content.

GenHub/GenHub.Core/Models/Content/ContentAcquiredMessage.cs (1)

1-9: LGTM! Clean message type implementation.

The ContentAcquiredMessage record is well-designed for event-driven messaging using modern C# patterns. The single-parameter design with clear documentation makes its purpose and usage straightforward.

GenHub/GenHub/Features/Content/Services/Publishers/SuperHackersManifestFactory.cs (1)

360-360: LGTM! Theme color assignment is correctly conditioned on game type.

The theme color selection properly differentiates Zero Hour (dark red #8B0000) from Generals (orange #FFA500), consistent with the cover URL pattern on line 359.

GenHub/GenHub/Features/Content/Services/GeneralsOnline/GeneralsOnlineManifestFactory.cs (1)

86-86: LGTM! Theme color consistently applied across all GeneralsOnline manifest variants.

The GeneralsOnlineConstants.ThemeColor value is properly propagated to:

  • 30Hz and 60Hz game client variants
  • QuickMatch MapPack manifest
  • All manifest creation paths (initial, extracted, and local install)

The consistent use of a single theme color for the publisher is appropriate.

Also applies to: 313-313, 379-380, 405-406, 431-431

GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameClients/GameClientDetectorTests.cs (1)

5-5: LGTM! Consistent test mock updates.

All mock setups for AddManifestAsync have been correctly updated to include the new IProgress<ContentStorageProgress> parameter. The changes are consistent across all eight test methods.

Also applies to: 97-98, 147-148, 189-190, 292-293, 385-386, 468-469, 562-563, 613-614

GenHub/GenHub.Core/Interfaces/Manifest/IContentManifestPool.cs (1)

27-30: LGTM! Well-designed interface extension.

The addition of the optional progress parameter with a default value of null is well-designed. This maintains backward compatibility while enabling progress reporting for storage operations. The documentation is clear and the parameter positioning (before CancellationToken) follows .NET conventions.

GenHub/GenHub.Core/Interfaces/GameProfiles/IGameClientProfileService.cs (1)

23-32: LGTM! Consistent theming support added.

The addition of the optional themeColor parameter to both profile creation methods is well-designed and consistent. The parameter ordering (after visual elements like icon and cover) is logical, and the default null value maintains backward compatibility.

Also applies to: 43-52

GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameClients/GameClientManifestIntegrationTests.cs (4)

27-27: LGTM! Appropriate for integration tests.

Changing from IFileHashProvider to the concrete Sha256HashProvider type is appropriate for integration tests, as it ensures deterministic SHA-256 hash computation. This provides more predictable test behavior compared to a mocked interface.


49-50: LGTM! Mock setup correctly updated.

The mock setup for AddManifestAsync has been correctly updated to include the new IProgress<ContentStorageProgress> parameter, aligning with the interface change.


82-82: LGTM! Modern C# syntax improvements.

Good use of C# 12 collection expressions and direct indexing. The change from .First() to [0] is safe here since the test asserts Single(result.Items) on line 85, ensuring exactly one item exists.

Also applies to: 87-87


167-167: LGTM! Best practice for IDisposable pattern.

Adding GC.SuppressFinalize(this) is a best practice in the Dispose method, even though the class doesn't currently have a finalizer. This follows the standard IDisposable pattern and prevents potential issues if a finalizer is added in the future.

GenHub/GenHub/Features/Manifest/ContentManifestPool.cs (2)

77-80: LGTM: Progress reporting integration

The addition of the IProgress<ContentStorageProgress>? parameter to the method signature is well-documented and follows the established pattern of optional progress reporting throughout the codebase.


102-102: LGTM: Progress propagation to storage service

The progress reporter is correctly passed through to StoreContentAsync, enabling storage progress visibility.

GenHub/GenHub/Features/GameInstallations/GameInstallationService.cs (1)

386-386: LGTM: Call site updated for new signature

The call to AddManifestAsync correctly passes null for the new progress parameter, which is appropriate for this manifest registration scenario where progress reporting is not needed.

GenHub/GenHub/Features/GameProfiles/Services/ProfileLauncherFacade.cs (2)

861-871: LGTM: Optional dependency support implemented correctly

The validation logic correctly respects the IsOptional flag, only adding errors for non-optional dependencies while maintaining observability through logging for all cases.


924-934: LGTM: Consistent optional dependency handling

All dependency validation checks follow the same pattern: construct error messages, add to errors only for non-optional dependencies, and log with optional status. This ensures consistent behavior across all validation types (ID-based, version, game type, publisher type).

Also applies to: 944-956, 974-984, 994-1006, 1021-1033

GenHub/GenHub/Features/Content/Services/Publishers/GitHubManifestFactory.cs (3)

27-40: LGTM: Publisher matching logic

The CanHandle method correctly identifies GitHub content by checking the PublisherType. The comments explain the rationale for broad acceptance as a fallback factory.


50-54: LGTM: Early validation

Checking for directory existence before processing prevents unnecessary work and provides clear error handling through logging and early return.


90-108: LGTM: Manifest cloning preserves metadata

The manifest construction correctly preserves all original metadata (IDs, names, versions, dependencies, etc.) while replacing only the Files collection with the newly scanned content. This ensures consistency with the original manifest structure.

GenHub/GenHub/Features/Content/Services/GitHub/GitHubContentDeliverer.cs (2)

315-329: LGTM: Storage progress adapter correctly maps to acquisition progress

The storage progress adapter correctly maps ContentStorageProgress to ContentAcquisitionProgress for the StoringInCas phase, with progress ranging from 90-100%. The file count and current file information are properly propagated.


329-329: LGTM: Progress propagation to manifest pool

The storageProgress adapter is correctly passed to AddManifestAsync, enabling storage progress visibility throughout the content acquisition pipeline.

GenHub/GenHub/Features/GameProfiles/ViewModels/GameProfileSettingsViewModel.cs (2)

32-32: LGTM on messaging interface implementation.

The class correctly implements IRecipient<ContentAcquiredMessage> to enable message-driven refresh of available content when new content is acquired.


2130-2150: LGTM on dialog result handling.

The dialog flow correctly checks the result and the created content item before adding to the available content list and enabling it.

GenHub/GenHub/Features/GameProfiles/Services/GameClientProfileService.cs (3)

38-38: LGTM on theme color parameter addition.

The optional themeColor parameter is properly added with a null default, and the fallback logic on line 90 correctly uses the publisher-aware GetThemeColorForGameType when no explicit color is provided.

Also applies to: 90-90


368-394: Well-structured publisher-specific theming logic.

The GetThemeColorForGameType method properly handles:

  1. TheSuperHackers with game-type-specific colors (ZeroHour vs Generals)
  2. GeneralsOnline with its dark blue theme
  3. CommunityOutpost with green theme
  4. Returns null for unknown publishers to defer to manifest colors

This aligns well with the constants defined in SuperHackersConstants, GeneralsOnlineConstants, and CommunityOutpostConstants from the relevant code snippets.


269-269: No action required. The ThemeColor property exists on the ContentMetadata class as a public nullable string property (GenHub/GenHub.Core/Models/Manifest/ContentMetadata.cs, line 46). The code correctly accesses this property, and it is properly integrated throughout the codebase.

GenHub/GenHub/Features/Downloads/ViewModels/PublisherCardViewModel.cs (3)

390-394: LGTM on progress status formatting improvement.

The early return when CurrentOperation is non-empty provides more specific progress feedback to users, prioritizing operational details over generic phase information.


447-450: Good fix to restrict userVersion "0" skip to GameClient only.

The guard now correctly only skips local client manifests (with userVersion "0") when the content type is GameClient, preventing unintended filtering of other content types like MapPacks.


572-596: LGTM on improved download result handling.

The pattern matching result.Data is Core.Models.Manifest.ContentManifest manifest is cleaner and the ContentAcquiredMessage dispatch is properly wrapped in try-catch to prevent download flow failures from messaging issues.

GenHub/GenHub/Features/GameProfiles/Views/GameProfileSettingsWindow.axaml (1)

13-13: LGTM!

The addition of x:Name="SettingsWindow" and the refactoring to move the Add Local Content dialog to a separate window is a clean separation of concerns. The remaining code maintains consistent styling and structure.

GenHub/GenHub/Features/GameProfiles/Views/AddLocalContentWindow.axaml (1)

1-21: Well-structured window implementation.

Good use of Avalonia features including acrylic transparency, drag-drop support, and custom chrome. The two-column layout with form and preview pane provides clear UX.

GenHub/GenHub/Features/GameProfiles/ViewModels/AddLocalContentViewModel.cs (2)

16-46: Clean MVVM implementation with CommunityToolkit.Mvvm.

Good use of primary constructor pattern, [ObservableProperty] attributes, and [RelayCommand] for commands. The static property pattern for available types is appropriate.


135-211: Well-implemented import flow with proper async handling.

The import logic correctly handles both ZIP archives and folders, uses Task.Run for blocking operations, and includes automatic map file organization. Progress and status updates provide good UX feedback.

ContentType.Map,
ContentType.MapPack,
ContentType.Mission,
ContentType.Mod,
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for ContentType.Mods usage and find relevant files
rg -nP --type=cs '\bContentType\.Mods\b' -C3

Repository: undead2146/GenHub

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Find and read ContentType.cs file
fd -e cs "ContentType.cs" -x cat -n {}

Repository: undead2146/GenHub

Length of output: 2283


🏁 Script executed:

#!/bin/bash
# Find and read LocalContentService.cs file, focusing on lines around 37 and 54
fd -e cs "LocalContentService.cs" -x head -n 70 {}

Repository: undead2146/GenHub

Length of output: 3892


🏁 Script executed:

#!/bin/bash
# Search for Mods references in C# files to understand usage pattern
rg -nP --type=cs '\bMods\b' | head -30

Repository: undead2146/GenHub

Length of output: 1071


Verify handling of the ContentType.Mods enum value.

ContentType.Mods is defined in ContentType.cs (line 25) as "Major gameplay changes (alias for Mod)" but is not included in AllowedContentTypes. The validation check will reject ContentType.Mods, creating an inconsistency. However, ContentType.Mods is not used anywhere in the codebase, suggesting the enum value is redundant.

Either:

  1. Remove ContentType.Mods from the enum entirely (preferred—eliminate unused alias)
  2. Add ContentType.Mods to AllowedContentTypes if it must coexist with Mod

Clarify the intended usage pattern.

🤖 Prompt for AI Agents
In GenHub/GenHub.Core/Services/Content/LocalContentService.cs around line 37 and
ContentType.cs at line 25, the enum value ContentType.Mods (alias for Mod) is
inconsistent because AllowedContentTypes does not include Mods; fix by removing
the redundant ContentType.Mods entry from ContentType.cs (preferred) and then
run a solution-wide search to remove/update any references or tests; if you must
keep the alias instead, add ContentType.Mods to AllowedContentTypes in
LocalContentService and ensure validation and tests accept it.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
GenHub/GenHub/Features/Content/Services/GitHub/GitHubContentDeliverer.cs (1)

141-145: Update log message to reflect expanded content types.

The condition now includes ContentType.Mod, but the log message at line 143 still only mentions "GameClient content detected."

🔎 Suggested fix
-               logger.LogInformation(
-                   "GameClient content detected with {Count} ZIP files. Extracting...",
-                   zipFiles.Count);
+               logger.LogInformation(
+                   "{ContentType} content detected with {Count} ZIP files. Extracting...",
+                   packageManifest.ContentType,
+                   zipFiles.Count);
♻️ Duplicate comments (1)
GenHub/GenHub/Features/GameProfiles/ViewModels/GameProfileSettingsViewModel.cs (1)

2106-2106: Fix typo in status message.

"unavailble" should be "unavailable".

🔎 Proposed fix
-                StatusMessage = "Local content service unavailble";
+                StatusMessage = "Local content service unavailable";
🧹 Nitpick comments (6)
GenHub/GenHub/Features/GameProfiles/ViewModels/GameProfileSettingsViewModel.cs (3)

99-101: Consider explicit cleanup of messenger registration.

While WeakReferenceMessenger uses weak references, explicitly unregistering in a cleanup method (e.g., implementing IDisposable or overriding a base cleanup method) ensures deterministic resource management and prevents potential memory leaks in long-running applications.

🔎 Suggested cleanup pattern

If ViewModelBase supports it, override or implement cleanup:

protected override void OnDeactivated()
{
    WeakReferenceMessenger.Default.Unregister<Core.Models.Content.ContentAcquiredMessage>(this);
    base.OnDeactivated();
}

Or implement IDisposable:

public void Dispose()
{
    WeakReferenceMessenger.Default.Unregister<Core.Models.Content.ContentAcquiredMessage>(this);
}

504-512: Consider adding error logging to the message handler.

The fire-and-forget pattern is acceptable here, but wrapping the call in a try-catch block within Receive could provide better diagnostics if LoadAvailableContentAsync fails during message handling.

🔎 Optional improvement
 public void Receive(Core.Models.Content.ContentAcquiredMessage message)
 {
-    // Refresh available content when new content is acquired
-    _ = LoadAvailableContentAsync();
+    try
+    {
+        // Refresh available content when new content is acquired
+        _ = LoadAvailableContentAsync();
+    }
+    catch (Exception ex)
+    {
+        logger?.LogError(ex, "Error refreshing content after ContentAcquiredMessage");
+    }
 }

2123-2123: Logger null handling improved, but consider using ILoggerFactory.

Passing null is safer than the previous cast approach, but leaves AddLocalContentViewModel without logging. If diagnostic logging is valuable for this dialog, inject ILoggerFactory into this ViewModel and create a properly-typed logger.

🔎 Suggested improvement using ILoggerFactory

If ILoggerFactory is available as a constructor parameter:

-            var vm = new AddLocalContentViewModel(localContentService, null);
+            var addContentLogger = loggerFactory?.CreateLogger<AddLocalContentViewModel>();
+            var vm = new AddLocalContentViewModel(localContentService, addContentLogger);
GenHub/GenHub/Common/ViewModels/MainViewModel.cs (1)

95-98: Add a comment explaining why Tools tab is disallowed as the initial tab.

The silent redirect from NavigationTab.Tools to GameProfiles lacks an explanation. Future maintainers won't understand the rationale for this behavior.

🔎 Proposed fix
             var tab = configurationProvider.GetLastSelectedTab();
+            // Tools tab requires additional initialization that isn't ready at startup,
+            // so redirect to GameProfiles as the default landing tab.
             if (tab == NavigationTab.Tools)
             {
                 tab = NavigationTab.GameProfiles;
             }
GenHub/GenHub/Features/Content/Services/GitHub/GitHubContentDeliverer.cs (1)

157-182: Consider checking cancellation token during extraction.

The extraction loop processes entries without checking cancellationToken. For large archives, this could delay cancellation response.

🔎 Suggested addition
                            foreach (var entry in archive.Entries)
                            {
+                               cancellationToken.ThrowIfCancellationRequested();
+
                                if (string.IsNullOrEmpty(entry.Name)) continue; // Skip directories
GenHub/GenHub/Features/GameProfiles/ViewModels/AddLocalContentViewModel.cs (1)

217-228: Consider indicating when preview is truncated.

The preview limits (20 directories, 50 files) prevent performance issues but give no visual feedback when content is truncated. Users importing large folders might be confused seeing fewer items than expected.

💡 Optional enhancement

Consider adding a status message or tree node indicating truncation:

 private static List<FileTreeItem> BuildDirectoryTree(DirectoryInfo dir)
 {
     var items = new List<FileTreeItem>();
+    var directories = dir.GetDirectories();
+    var files = dir.GetFiles();

-    foreach (var d in dir.GetDirectories().Take(20))
+    foreach (var d in directories.Take(20))
     {
         items.Add(new FileTreeItem
         {
             Name = d.Name,
             IsFile = false,
             FullPath = d.FullName,
             Children = new ObservableCollection<FileTreeItem>(BuildDirectoryTree(d)),
         });
     }

-    foreach (var f in dir.GetFiles().Take(50))
+    if (directories.Length > 20)
+    {
+        items.Add(new FileTreeItem { Name = $"... and {directories.Length - 20} more directories", IsFile = false, FullPath = string.Empty });
+    }
+
+    foreach (var f in files.Take(50))
     {
         items.Add(new FileTreeItem { Name = f.Name, IsFile = true, FullPath = f.FullName });
     }
+
+    if (files.Length > 50)
+    {
+        items.Add(new FileTreeItem { Name = $"... and {files.Length - 50} more files", IsFile = true, FullPath = string.Empty });
+    }

     return items;
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9123c1c and 7542426.

📒 Files selected for processing (6)
  • .github/scripts/inject-token.ps1
  • .github/workflows/ci.yml
  • GenHub/GenHub/Common/ViewModels/MainViewModel.cs
  • GenHub/GenHub/Features/Content/Services/GitHub/GitHubContentDeliverer.cs
  • GenHub/GenHub/Features/GameProfiles/ViewModels/AddLocalContentViewModel.cs
  • GenHub/GenHub/Features/GameProfiles/ViewModels/GameProfileSettingsViewModel.cs
💤 Files with no reviewable changes (1)
  • .github/workflows/ci.yml
🧰 Additional context used
🧬 Code graph analysis (1)
GenHub/GenHub/Features/GameProfiles/ViewModels/AddLocalContentViewModel.cs (2)
GenHub/GenHub/Features/GameProfiles/ViewModels/FileTreeItem.cs (1)
  • FileTreeItem (8-29)
GenHub/GenHub/Infrastructure/Extensions/LoggerExtensions.cs (1)
  • LogError (46-51)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build Linux
  • GitHub Check: Build Windows
🔇 Additional comments (11)
GenHub/GenHub/Features/GameProfiles/ViewModels/GameProfileSettingsViewModel.cs (2)

32-32: LGTM! Message-driven architecture implemented correctly.

The IRecipient<ContentAcquiredMessage> interface enables automatic refresh of available content when new content is acquired, following MVVM Community Toolkit patterns.


2131-2149: LGTM! Local content workflow implemented correctly.

The dialog result handling properly adds the new content to the available list, enables it with the loading guard bypassed (appropriate for user-initiated actions), and provides user feedback via notification.

.github/scripts/inject-token.ps1 (1)

6-13: Approve the fork PR handling approach.

The logic correctly handles missing tokens in fork PRs by using a dummy token while failing non-PR builds appropriately. The 'pull_request' event check is the correct condition since fork PRs don't have access to secrets.

The application code handles the dummy token gracefully. In UploadThingService.cs, if the token deobfuscates but the UploadThing API rejects it as invalid, the service logs an error and returns null rather than throwing an exception. All callers check for null and handle it appropriately, allowing the build to pass without uploads.

GenHub/GenHub/Common/ViewModels/MainViewModel.cs (5)

45-58: Primary constructor with dependency injection looks good.

The use of C# 12 primary constructor syntax with 14 dependencies is acceptable for a main shell ViewModel that orchestrates multiple feature view models and services. The nullable logger with default null is a reasonable pattern for optional logging.


65-88: Property initialization follows CommunityToolkit.Mvvm patterns correctly.

The [ObservableProperty] attribute and initialization from primary constructor parameters is idiomatic for the MVVM Community Toolkit.


166-175: Initialization flow with optional logging is correct.

The null-conditional logger usage and background update check pattern are appropriate.


190-254: Update checking logic with proper service injection and error handling.

The method correctly uses injected services, propagates cancellation tokens, dispatches UI notifications on the UI thread, and handles exceptions appropriately.


256-270: Background update check wrapper handles exceptions correctly.

Separating OperationCanceledException from other exceptions is the right approach for cancellable background operations.

GenHub/GenHub/Features/Content/Services/GitHub/GitHubContentDeliverer.cs (1)

315-329: LGTM!

The storage progress adapter cleanly maps ContentStorageProgress to ContentAcquisitionProgress with appropriate null handling for CurrentFileName.

GenHub/GenHub/Features/GameProfiles/ViewModels/AddLocalContentViewModel.cs (2)

345-345: Previous issue resolved: targetGame now correctly uses SelectedGameType.

The hardcoded fallback to GameType.Generals mentioned in the previous review has been fixed. The code now properly respects the user's game selection for all content types.


425-427: Previous issue resolved: Null check added for parentDir.

The null-forgiving operator issue from the previous review has been fixed. Line 426 now includes an explicit null check with early continuation, preventing potential NullReferenceException when Path.GetDirectoryName returns null.

Comment on lines +276 to 293
userSettingsService.Update(settings =>
{
settings.LastSelectedTab = selectedTab;
});

_ = _userSettingsService.SaveAsync();
_logger?.LogDebug("Updated last selected tab to: {Tab}", selectedTab);
_ = userSettingsService.SaveAsync();
logger?.LogDebug("Updated last selected tab to: {Tab}", selectedTab);
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to update selected tab setting");
logger?.LogError(ex, "Failed to update selected tab setting");
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fire-and-forget async may silently lose exceptions.

The _ = userSettingsService.SaveAsync() discards the task without observing it. If SaveAsync throws after the synchronous portion completes, the exception won't be caught by the surrounding try-catch block, potentially losing the user's tab preference without any indication.

🔎 Proposed fix: Observe the task with a continuation
-            _ = userSettingsService.SaveAsync();
+            _ = userSettingsService.SaveAsync().ContinueWith(
+                t => logger?.LogError(t.Exception, "Failed to save user settings"),
+                TaskContinuationOptions.OnlyOnFaulted);
             logger?.LogDebug("Updated last selected tab to: {Tab}", selectedTab);
📝 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
userSettingsService.Update(settings =>
{
settings.LastSelectedTab = selectedTab;
});
_ = _userSettingsService.SaveAsync();
_logger?.LogDebug("Updated last selected tab to: {Tab}", selectedTab);
_ = userSettingsService.SaveAsync();
logger?.LogDebug("Updated last selected tab to: {Tab}", selectedTab);
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to update selected tab setting");
logger?.LogError(ex, "Failed to update selected tab setting");
}
userSettingsService.Update(settings =>
{
settings.LastSelectedTab = selectedTab;
});
_ = userSettingsService.SaveAsync().ContinueWith(
t => logger?.LogError(t.Exception, "Failed to save user settings"),
TaskContinuationOptions.OnlyOnFaulted);
logger?.LogDebug("Updated last selected tab to: {Tab}", selectedTab);
}
catch (Exception ex)
{
logger?.LogError(ex, "Failed to update selected tab setting");
}
🤖 Prompt for AI Agents
In GenHub/GenHub/Common/ViewModels/MainViewModel.cs around lines 276-287 the
call `_ = userSettingsService.SaveAsync()` is fire-and-forget which can silently
lose exceptions; either await the Task (make the calling method async and await
userSettingsService.SaveAsync()) so exceptions propagate into the existing
try/catch, or if you cannot make the method async capture the Task and attach a
continuation that logs failures (e.g.,
userSettingsService.SaveAsync().ContinueWith(t => { if (t.IsFaulted)
logger?.LogError(t.Exception, "Failed to save user settings"); },
TaskContinuationOptions.ExecuteSynchronously)); ensure exceptions from SaveAsync
are observed and logged.

Comment on lines +98 to +123
// Create progress adapter for download progress
IProgress<DownloadProgress>? downloadProgress = null;
if (progress != null)
{
downloadProgress = new Progress<DownloadProgress>(dp =>
{
// Map download progress (0-100) to the Downloading phase range (40-65%)
// We start at 40 (ProgressStepDownloading) and use 25% of the range for downloads
double downloadRange = 25.0; // 40% to 65%
double fileProgressRange = downloadRange / totalFiles;
double baseProgress = ContentConstants.ProgressStepDownloading + ((currentFileIndex - 1) * fileProgressRange);
double currentProgress = baseProgress + (dp.Percentage / 100.0 * fileProgressRange);

progress.Report(new ContentAcquisitionProgress
{
Phase = ContentAcquisitionPhase.Downloading,
ProgressPercentage = currentProgress,
CurrentOperation = $"{file.RelativePath} ({currentFileIndex}/{totalFiles}) - {dp.Percentage:F0}% ({dp.FormattedSpeed})",
FilesProcessed = currentFileIndex - 1,
TotalFiles = totalFiles,
TotalBytes = dp.TotalBytes,
BytesProcessed = dp.BytesReceived,
CurrentFile = file.RelativePath,
});
});
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Captured loop variable in closure may cause incorrect progress reporting.

The currentFileIndex variable is captured by reference in the lambda (line 102). Since Progress<T> posts callbacks asynchronously, the callback may execute after currentFileIndex has been incremented in the next loop iteration, causing incorrect file index reporting.

🔎 Suggested fix: capture the value explicitly
                // Create progress adapter for download progress
                IProgress<DownloadProgress>? downloadProgress = null;
                if (progress != null)
                {
+                   int capturedFileIndex = currentFileIndex;
                    downloadProgress = new Progress<DownloadProgress>(dp =>
                    {
                        // Map download progress (0-100) to the Downloading phase range (40-65%)
                        // We start at 40 (ProgressStepDownloading) and use 25% of the range for downloads
                        double downloadRange = 25.0; // 40% to 65%
                        double fileProgressRange = downloadRange / totalFiles;
-                       double baseProgress = ContentConstants.ProgressStepDownloading + ((currentFileIndex - 1) * fileProgressRange);
+                       double baseProgress = ContentConstants.ProgressStepDownloading + ((capturedFileIndex - 1) * fileProgressRange);
                        double currentProgress = baseProgress + (dp.Percentage / 100.0 * fileProgressRange);

                        progress.Report(new ContentAcquisitionProgress
                        {
                            Phase = ContentAcquisitionPhase.Downloading,
                            ProgressPercentage = currentProgress,
-                           CurrentOperation = $"{file.RelativePath} ({currentFileIndex}/{totalFiles}) - {dp.Percentage:F0}% ({dp.FormattedSpeed})",
-                           FilesProcessed = currentFileIndex - 1,
+                           CurrentOperation = $"{file.RelativePath} ({capturedFileIndex}/{totalFiles}) - {dp.Percentage:F0}% ({dp.FormattedSpeed})",
+                           FilesProcessed = capturedFileIndex - 1,
                            TotalFiles = totalFiles,
                            TotalBytes = dp.TotalBytes,
                            BytesProcessed = dp.BytesReceived,
                            CurrentFile = file.RelativePath,
                        });
                    });
                }
🤖 Prompt for AI Agents
In GenHub/GenHub/Features/Content/Services/GitHub/GitHubContentDeliverer.cs
around lines 98 to 123, the lambda passed to new Progress<DownloadProgress>
closes over loop variables (currentFileIndex and file) so the async callback can
observe mutated values; fix this by capturing the needed values into local,
immutable variables immediately before creating the Progress (e.g., var
capturedIndex = currentFileIndex; var capturedFilePath = file.RelativePath) and
use those locals inside the lambda so each callback reports the correct file
index and path.

Comment on lines +157 to +165
foreach (var entry in archive.Entries)
{
if (string.IsNullOrEmpty(entry.Name)) continue; // Skip directories

var destinationPath = Path.Combine(targetDirectory, entry.FullName);
var destinationDir = Path.GetDirectoryName(destinationPath);
if (!string.IsNullOrEmpty(destinationDir)) Directory.CreateDirectory(destinationDir);

entry.ExtractToFile(destinationPath, overwrite: true);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Zip Slip vulnerability: validate extracted file paths stay within target directory.

entry.FullName from a ZIP archive can contain path traversal sequences like ../ that could write files outside the intended targetDirectory. This is a known attack vector (Zip Slip).

🔎 Suggested fix: validate the destination path
                            foreach (var entry in archive.Entries)
                            {
                                if (string.IsNullOrEmpty(entry.Name)) continue; // Skip directories

                                var destinationPath = Path.Combine(targetDirectory, entry.FullName);
+                               var fullDestinationPath = Path.GetFullPath(destinationPath);
+                               var fullTargetDirectory = Path.GetFullPath(targetDirectory + Path.DirectorySeparatorChar);
+
+                               if (!fullDestinationPath.StartsWith(fullTargetDirectory, StringComparison.OrdinalIgnoreCase))
+                               {
+                                   logger.LogWarning("Skipping potentially malicious ZIP entry: {EntryName}", entry.FullName);
+                                   continue;
+                               }
+
                                var destinationDir = Path.GetDirectoryName(destinationPath);
                                if (!string.IsNullOrEmpty(destinationDir)) Directory.CreateDirectory(destinationDir);

                                entry.ExtractToFile(destinationPath, overwrite: true);
🤖 Prompt for AI Agents
In GenHub/GenHub/Features/Content/Services/GitHub/GitHubContentDeliverer.cs
around lines 157-165, the code directly uses entry.FullName to build
destinationPath which can allow path traversal (Zip Slip); resolve the
destination path with Path.GetFullPath(Path.Combine(targetDirectory,
entry.FullName)) and compare it to the normalized targetDirectory full path
(ensure targetDirFullPath ends with a directory separator and check
destinationFullPath.StartsWith(targetDirFullPath,
StringComparison.OrdinalIgnoreCase)); if the check fails, skip the entry or
throw an exception; otherwise create the destination directory from the
validated destinationFullPath and call ExtractToFile safely with overwrite:
true.

Comment on lines 164 to 196
if (File.Exists(path))
{
var extension = Path.GetExtension(path);
if (extension.Equals(".zip", StringComparison.OrdinalIgnoreCase))
{
await Task.Run(() => ZipFile.ExtractToDirectory(path, _stagingPath, true));
}
else
{
var destFile = Path.Combine(_stagingPath, Path.GetFileName(path));
File.Copy(path, destFile, true);
}
}
else if (Directory.Exists(path))
{
var dirInfo = new DirectoryInfo(path);
var dirName = dirInfo.Name;

// Ensure we don't try to copy to the staging root itself if Name is somehow empty
if (string.IsNullOrWhiteSpace(dirName))
{
dirName = "Imported_Folder";
}

var targetSubDir = Path.Combine(_stagingPath, dirName);
logger?.LogDebug("ImportContentAsync: Preserving directory structure. Source: {Source}, Target: {Target}", path, targetSubDir);

await Task.Run(() => CopyDirectory(dirInfo, new DirectoryInfo(targetSubDir)));
}

// Auto-organization: If we have .map files at the root level, move them into subdirectories
// This is required because the game expects maps to be in their own folders
CreateMapFoldersIfNeeded();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Wrap remaining blocking I/O operations in Task.Run.

While ZIP extraction (line 169) and directory copying (line 191) are correctly offloaded to the thread pool, File.Copy at line 174 and the CreateMapFoldersIfNeeded call at line 196 remain blocking. For large files or directories with many .map files, these synchronous operations could freeze the UI.

🔎 Proposed fix
             if (File.Exists(path))
             {
                 var extension = Path.GetExtension(path);
                 if (extension.Equals(".zip", StringComparison.OrdinalIgnoreCase))
                 {
                     await Task.Run(() => ZipFile.ExtractToDirectory(path, _stagingPath, true));
                 }
                 else
                 {
-                    var destFile = Path.Combine(_stagingPath, Path.GetFileName(path));
-                    File.Copy(path, destFile, true);
+                    await Task.Run(() =>
+                    {
+                        var destFile = Path.Combine(_stagingPath, Path.GetFileName(path));
+                        File.Copy(path, destFile, true);
+                    });
                 }
             }
             else if (Directory.Exists(path))
             {
                 var dirInfo = new DirectoryInfo(path);
                 var dirName = dirInfo.Name;

                 // Ensure we don't try to copy to the staging root itself if Name is somehow empty
                 if (string.IsNullOrWhiteSpace(dirName))
                 {
                     dirName = "Imported_Folder";
                 }

                 var targetSubDir = Path.Combine(_stagingPath, dirName);
                 logger?.LogDebug("ImportContentAsync: Preserving directory structure. Source: {Source}, Target: {Target}", path, targetSubDir);

                 await Task.Run(() => CopyDirectory(dirInfo, new DirectoryInfo(targetSubDir)));
             }

             // Auto-organization: If we have .map files at the root level, move them into subdirectories
             // This is required because the game expects maps to be in their own folders
-            CreateMapFoldersIfNeeded();
+            await Task.Run(CreateMapFoldersIfNeeded);
🤖 Prompt for AI Agents
In GenHub/GenHub/Features/GameProfiles/ViewModels/AddLocalContentViewModel.cs
around lines 164 to 196, the File.Copy call and the CreateMapFoldersIfNeeded
invocation are synchronous and can block the UI for large operations; wrap the
File.Copy operation in Task.Run and await it (e.g. await Task.Run(() =>
File.Copy(...))) and likewise run CreateMapFoldersIfNeeded on the thread pool
(await Task.Run(() => CreateMapFoldersIfNeeded())) or make
CreateMapFoldersIfNeeded async and await its async implementation so both file
I/O and map-folder creation are executed off the UI thread.

Comment on lines +287 to +305
try
{
IsBusy = true;
StatusMessage = $"Removing {item.Name}...";
logger?.LogInformation("Deleting item from staging: {Name} ({Path})", item.Name, item.FullPath);

if (item.IsFile && File.Exists(item.FullPath))
{
File.Delete(item.FullPath);
}
else if (!item.IsFile && Directory.Exists(item.FullPath))
{
Directory.Delete(item.FullPath, true);
}

await RefreshStagingTreeAsync();
StatusMessage = $"Removed {item.Name}.";
logger?.LogInformation("Item successfully deleted: {Name}", item.Name);
Validate();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Offload file and directory deletion to background thread.

File.Delete (line 295) and Directory.Delete with recursive: true (line 299) are blocking operations. Deleting large files or deep directory trees can freeze the UI.

🔎 Proposed fix
         try
         {
             IsBusy = true;
             StatusMessage = $"Removing {item.Name}...";
             logger?.LogInformation("Deleting item from staging: {Name} ({Path})", item.Name, item.FullPath);

-            if (item.IsFile && File.Exists(item.FullPath))
-            {
-                File.Delete(item.FullPath);
-            }
-            else if (!item.IsFile && Directory.Exists(item.FullPath))
-            {
-                Directory.Delete(item.FullPath, true);
-            }
+            await Task.Run(() =>
+            {
+                if (item.IsFile && File.Exists(item.FullPath))
+                {
+                    File.Delete(item.FullPath);
+                }
+                else if (!item.IsFile && Directory.Exists(item.FullPath))
+                {
+                    Directory.Delete(item.FullPath, true);
+                }
+            });

             await RefreshStagingTreeAsync();
             StatusMessage = $"Removed {item.Name}.";
             logger?.LogInformation("Item successfully deleted: {Name}", item.Name);
📝 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
try
{
IsBusy = true;
StatusMessage = $"Removing {item.Name}...";
logger?.LogInformation("Deleting item from staging: {Name} ({Path})", item.Name, item.FullPath);
if (item.IsFile && File.Exists(item.FullPath))
{
File.Delete(item.FullPath);
}
else if (!item.IsFile && Directory.Exists(item.FullPath))
{
Directory.Delete(item.FullPath, true);
}
await RefreshStagingTreeAsync();
StatusMessage = $"Removed {item.Name}.";
logger?.LogInformation("Item successfully deleted: {Name}", item.Name);
Validate();
try
{
IsBusy = true;
StatusMessage = $"Removing {item.Name}...";
logger?.LogInformation("Deleting item from staging: {Name} ({Path})", item.Name, item.FullPath);
await Task.Run(() =>
{
if (item.IsFile && File.Exists(item.FullPath))
{
File.Delete(item.FullPath);
}
else if (!item.IsFile && Directory.Exists(item.FullPath))
{
Directory.Delete(item.FullPath, true);
}
});
await RefreshStagingTreeAsync();
StatusMessage = $"Removed {item.Name}.";
logger?.LogInformation("Item successfully deleted: {Name}", item.Name);
Validate();
🤖 Prompt for AI Agents
In GenHub/GenHub/Features/GameProfiles/ViewModels/AddLocalContentViewModel.cs
around lines 287-305, the synchronous File.Delete and Directory.Delete calls
block the UI; move the deletion work onto a background thread (e.g., wrap the
file/directory delete logic in Task.Run and await it) so the UI thread stays
responsive, preserve logging and exception handling inside that background task,
and ensure IsBusy/StatusMessage are updated on the UI thread before and after
awaiting the Task.Run and that RefreshStagingTreeAsync is awaited after the
delete completes.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
GenHub/GenHub/Common/ViewModels/MainViewModel.cs (1)

277-295: Fire-and-forget async may silently lose exceptions.

Similar to the issue at line 268, the fire-and-forget call to DownloadsViewModel.OnTabActivatedAsync() at line 291 discards the task without observing it. If the async method throws after its synchronous portion completes, the exception will be unobserved.

🔎 Proposed fix: Observe the task with error logging
         else if (value == NavigationTab.Downloads)
         {
-            _ = DownloadsViewModel.OnTabActivatedAsync();
+            _ = DownloadsViewModel.OnTabActivatedAsync().ContinueWith(
+                t => logger?.LogError(t.Exception, "Failed to activate Downloads tab"),
+                TaskContinuationOptions.OnlyOnFaulted);
         }
♻️ Duplicate comments (3)
GenHub/GenHub/Features/Content/Services/GitHub/GitHubContentDeliverer.cs (2)

98-123: Loop variable capture issue persists.

The issue flagged in the previous review regarding currentFileIndex and file being captured by reference in the lambda remains unresolved. Since Progress<T> callbacks execute asynchronously, the callback may observe mutated values after the loop has advanced.


157-165: Zip Slip vulnerability remains unaddressed.

The security issue flagged in the previous review is still present. Line 161 uses entry.FullName directly without validating that the resolved path stays within targetDirectory, allowing potential path traversal attacks.

GenHub/GenHub/Common/ViewModels/MainViewModel.cs (1)

259-275: Fire-and-forget async at line 268 was previously flagged.

The fire-and-forget pattern _ = userSettingsService.SaveAsync() at line 268 remains unaddressed from previous review. As noted before, exceptions thrown after the synchronous portion completes won't be caught by the surrounding try-catch block.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7542426 and f259012.

📒 Files selected for processing (7)
  • .github/scripts/inject-token.ps1
  • .github/workflows/ci.yml
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/ViewModels/MainViewModelTests.cs
  • GenHub/GenHub/Common/ViewModels/MainViewModel.cs
  • GenHub/GenHub/Features/Content/Services/GitHub/GitHubContentDeliverer.cs
  • GenHub/GenHub/Features/GameProfiles/ViewModels/AddLocalContentViewModel.cs
  • GenHub/GenHub/Features/GameProfiles/ViewModels/GameProfileSettingsViewModel.cs
💤 Files with no reviewable changes (2)
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/ViewModels/MainViewModelTests.cs
  • .github/workflows/ci.yml
✅ Files skipped from review due to trivial changes (1)
  • GenHub/GenHub/Features/GameProfiles/ViewModels/AddLocalContentViewModel.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/scripts/inject-token.ps1
🧰 Additional context used
🧬 Code graph analysis (1)
GenHub/GenHub/Features/Content/Services/GitHub/GitHubContentDeliverer.cs (5)
GenHub/GenHub.Core/Models/Common/DownloadProgress.cs (1)
  • DownloadProgress (14-54)
GenHub/GenHub.Core/Constants/ContentConstants.cs (1)
  • ContentConstants (6-92)
GenHub/GenHub.Core/Models/Content/ContentAcquisitionProgress.cs (1)
  • ContentAcquisitionProgress (7-53)
GenHub/GenHub/Features/Content/Services/Helpers/GitHubInferenceHelper.cs (1)
  • ContentType (23-41)
GenHub/GenHub/Features/Content/Services/GitHub/GitHubResolver.cs (1)
  • ContentType (280-283)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build Windows
  • GitHub Check: Build Linux
🔇 Additional comments (10)
GenHub/GenHub/Features/GameProfiles/ViewModels/GameProfileSettingsViewModel.cs (4)

32-32: LGTM: Message recipient implementation.

The interface implementation follows the CommunityToolkit.Mvvm messaging pattern correctly and enables automatic content refresh when new content is acquired.


99-101: LGTM: Proper message registration.

The registration uses WeakReferenceMessenger, which prevents memory leaks by allowing the ViewModel to be garbage collected even without explicit unregistration.


504-512: LGTM: Message handler implementation.

The Receive method correctly implements the message handling pattern. The fire-and-forget invocation of LoadAvailableContentAsync() is appropriate here since the method has its own error handling and the IsLoadingContent guard prevents concurrent execution issues.


2096-2157: LGTM: Past review issues resolved.

The method correctly addresses previous review comments:

  • Typo "unavailble" → "unavailable" (Line 2106)
  • Unsafe logger cast → null parameter (Line 2123)

The dialog flow is well-implemented with proper owner resolution, result handling, and user feedback via notifications.

GenHub/GenHub/Features/Content/Services/GitHub/GitHubContentDeliverer.cs (3)

141-141: Good enhancement to include Mod content type.

Extending ZIP extraction to handle ContentType.Mod in addition to GameClient is appropriate, as mods often come packaged as ZIP archives.


168-181: Extraction progress reporting looks solid.

The progress mapping from 65% to 70% with per-entry reporting provides good visibility into the extraction phase.


315-329: Storage progress integration is well-implemented.

The storage progress adapter correctly maps ContentStorageProgress to ContentAcquisitionProgress with appropriate phase and percentage ranges (90-100%). The progress parameter is properly threaded through to AddManifestAsync.

GenHub/GenHub/Common/ViewModels/MainViewModel.cs (3)

22-72: Clean constructor injection pattern.

The transition to constructor-based dependency injection with public read-only properties is well-structured and follows best practices for testability and maintainability.


153-162: Background update check is appropriately fire-and-forget.

The async initialization and background update check are correctly structured with cancellation token support. The fire-and-forget pattern at line 161 is acceptable for background work since exceptions are handled in the wrapper method.


177-241: Update check implementation is solid.

The refactored update check logic correctly uses injected dependencies, properly handles cancellation, marshals UI updates to the correct thread, and includes comprehensive error handling.

…age and multi-instance debugging support (community-outpost#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.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI Agents
In @GenHub/GenHub/Features/Tools/MapManager/Services/TgaParser.cs:
- Around line 109-113: The guard in ConvertToRgba silently returns an all-zero
buffer when bgrData is too short; change ConvertToRgba to log a warning and
return null (use signature byte[]? ConvertToRgba(...)), and update the caller in
ParseTga to handle a null result; additionally ensure DecodeRle reports actual
filled length (or returns null/throws on truncation) so ConvertToRgba can verify
that the number of filled bytes matches expectedSrcSize before proceeding — if
the filled byte count is less than expectedSrcSize, log a warning and return
null for consistency with other error paths.
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f259012 and 55a9182.

📒 Files selected for processing (2)
  • GenHub/GenHub/Features/Tools/MapManager/Services/MapImportService.cs
  • GenHub/GenHub/Features/Tools/MapManager/Services/TgaParser.cs
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build Linux
  • GitHub Check: Build Windows
🔇 Additional comments (1)
GenHub/GenHub/Features/Tools/MapManager/Services/MapImportService.cs (1)

26-26: LGTM! Critical fix for Windows path support.

The escape sequence correction from '\' to '\\' is necessary for C# to properly represent a literal backslash character. This ensures Windows-style paths in ZIP archives are correctly split and processed at lines 245, 403, and 417.

Comment on lines 109 to 113
int expectedSrcSize = width * height * bytesPerPixel;
if (bgrData.Length < expectedSrcSize)
{
return rgba;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add logging and consider returning null for consistency.

The defensive guard silently returns an all-zero buffer when input data is insufficient, which is inconsistent with other error paths in this file (e.g., lines 25-26, 230-231, 250-251) that log a warning and return null. This inconsistency could hide data corruption issues by producing a valid but all-black image instead of a clear failure.

Additionally, note that this guard won't catch partially-filled buffers from DecodeRle (lines 58-101), since DecodeRle always allocates a full-size array (new byte[totalPixels * bytesPerPixel] at line 61) even if the RLE stream is truncated and only partially fills it. The array length check will pass even when the buffer content is incomplete.

Suggested improvements for consistent error handling
 var rgba = new byte[width * height * 4];
 int expectedSrcSize = width * height * bytesPerPixel;
 if (bgrData.Length < expectedSrcSize)
 {
-    return rgba;
+    logger.LogWarning("Insufficient BGR(A) data: expected {Expected} bytes, got {Actual}", expectedSrcSize, bgrData.Length);
+    return null;
 }

Then update the return type and calling code at line 300 to handle null:

private static byte[]? ConvertToRgba(byte[] bgrData, int width, int height, int bytesPerPixel, bool topToBottom)

And in ParseTga:

 // Convert BGR(A) to RGBA for Avalonia
 var rgbaData = ConvertToRgba(pixelData, width, height, bytesPerPixel, topToBottom);
+if (rgbaData == null)
+{
+    logger.LogWarning("Failed to convert TGA pixel data: {Path}", sourcePath);
+    return null;
+}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @GenHub/GenHub/Features/Tools/MapManager/Services/TgaParser.cs around lines
109-113, The guard in ConvertToRgba silently returns an all-zero buffer when
bgrData is too short; change ConvertToRgba to log a warning and return null (use
signature byte[]? ConvertToRgba(...)), and update the caller in ParseTga to
handle a null result; additionally ensure DecodeRle reports actual filled length
(or returns null/throws on truncation) so ConvertToRgba can verify that the
number of filled bytes matches expectedSrcSize before proceeding — if the filled
byte count is less than expectedSrcSize, log a warning and return null for
consistency with other error paths.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI Agents
In @GenHub/GenHub/Features/Tools/MapManager/Services/MapPackService.cs:
- Line 87: The manifest ID generation using ManifestId.Create with
ManifestConstants.DefaultManifestVersion currently only replaces spaces and can
produce invalid IDs when name contains other special characters; update the code
that builds the ID (the expression that uses name.ToLowerInvariant().Replace("
", "-")) to fully sanitize the content segment to match
PublisherContentRegexPattern by: lowercasing the name, replacing any character
not matching [a-z0-9-] with a hyphen, collapsing consecutive hyphens into one,
trimming leading/trailing hyphens, and ensuring a non-empty fallback (e.g.,
"unknown") before calling ManifestId.Create so the generated ID always
validates.

In @GenHub/GenHub/Features/Tools/Services/UploadHistoryService.cs:
- Around line 62-66: The private method InitializeInternalAsync currently only
calls RunCleanupAsync but is never used; either remove this dead method or wire
it into startup: add it to the IUploadHistoryService contract and call
InitializeInternalAsync from the service constructor (or the service
registration/startup routine) so initialization runs, or simply delete the
InitializeInternalAsync method and keep its logic where RunCleanupAsync is
invoked; reference InitializeInternalAsync and RunCleanupAsync when making the
change.

In @GenHub/GenHub/Infrastructure/Imaging/TgaImageParser.cs:
- Around line 98-101: The bounds-check in ConvertToRgba currently returns a
partially filled rgbaData on truncated sourceData; change ConvertToRgba's
signature to return byte[]? and, instead of returning rgbaData on that check,
log a warning via the class logger (include path/context if available) and
return null; then update the caller ParseTgaFile to handle a null return (check
the result, log "Failed to convert TGA data to RGBA: {Path}" or similar, and
propagate null) so truncated/decompress errors are surfaced rather than
producing corrupted images.
🧹 Nitpick comments (8)
GenHub/GenHub/Infrastructure/Converters/EnumToBoolConverter.cs (2)

14-22: LGTM! Improved null-safety and case-insensitive comparison.

The changes improve robustness with the dual-check approach (direct equality + string comparison) and case-insensitive string matching. The null-safe operators on line 21 are technically redundant given the early return on lines 16-19, but they serve as defensive programming and don't impact correctness.

Optional: Remove redundant null-safe operators

Since lines 16-19 already guarantee non-null values, the ?. operators on line 21 are unnecessary:

-        return value?.Equals(parameter) == true || value?.ToString()?.Equals(parameter?.ToString(), StringComparison.OrdinalIgnoreCase) == true;
+        return value.Equals(parameter) || value.ToString().Equals(parameter.ToString(), StringComparison.OrdinalIgnoreCase);

However, the current approach is acceptable as defensive programming.


25-36: Use case-insensitive parsing for consistency with Convert.

The switch to Enum.TryParse is a good improvement over Enum.Parse (safer, no exceptions). However, there's an inconsistency: the Convert method uses case-insensitive string comparison (line 21), but ConvertBack uses case-sensitive parsing by default. This asymmetry could cause binding failures when the parameter casing doesn't match the enum exactly.

🔎 Proposed fix for case sensitivity consistency
-            if (Enum.TryParse(targetType, parameter.ToString(), out var result))
+            if (Enum.TryParse(targetType, parameter.ToString(), ignoreCase: true, out var result))
             {
                 return result;
             }
GenHub/GenHub.Linux/GameInstallations/SteamInstallation.cs (1)

172-172: Consider returning an interface type for better encapsulation.

While the current implementation works correctly, returning List<string> exposes implementation details. Consider returning IEnumerable<string> or IReadOnlyList<string> to provide better encapsulation and flexibility for future changes. Since this is a private method, the impact is limited to this class, but maintaining the interface-based return type would align with general best practices.

🔎 Suggested refactor to use interface return type
-private List<string> GetSteamLibraryPaths()
+private IEnumerable<string> GetSteamLibraryPaths()
GenHub/GenHub/Features/Tools/MapManager/Views/MapManagerView.axaml (1)

282-283: Consider centralizing retention period and size limit values.

The footer hard-codes "14 days" and "10MB", which duplicates the RetentionDays constant and also appears at Line 204. If these values change, multiple locations need updates.

Consider exposing these as ViewModel properties bound from the constants to ensure consistency across the UI.

Example approach

In the ViewModel, add properties:

public string RetentionPeriodDisplay => $"{ReplayManagerConstants.RetentionDays} days";
public string MaxUploadSizeDisplay => "10MB"; // or derive from a constant

Then in XAML:

-<TextBlock Grid.Row="4" Text="Files are maintained for up to 14 days or until storage is full. Maximum 10MB per map."
+<TextBlock Grid.Row="4" Text="{Binding StorageNoticeText}"
GenHub/GenHub/Features/Tools/MapManager/Services/MapDirectoryService.cs (2)

234-254: Consider adding macOS support.

The implementation handles Windows and Linux platforms but doesn't provide support for macOS. macOS users would be unable to open directories using this feature.

Consider adding macOS support:

Suggested enhancement
 public void OpenDirectory(GameType version)
 {
     var directory = GetMapDirectory(version);
     EnsureDirectoryExists(version);

     try
     {
         if (OperatingSystem.IsWindows())
         {
             System.Diagnostics.Process.Start("explorer.exe", directory);
         }
         else if (OperatingSystem.IsLinux())
         {
             System.Diagnostics.Process.Start("xdg-open", directory);
         }
+        else if (OperatingSystem.IsMacOS())
+        {
+            System.Diagnostics.Process.Start("open", directory);
+        }
     }
     catch (Exception ex)
     {
         logger.LogError(ex, "Failed to open map directory: {Directory}", directory);
     }
 }

257-274: Consider adding macOS support.

The implementation handles Windows and Linux platforms but doesn't provide support for macOS. macOS users would be unable to reveal files using this feature.

Consider adding macOS support:

Suggested enhancement
 public void RevealFile(MapFile map)
 {
     try
     {
         if (OperatingSystem.IsWindows())
         {
             System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{map.FullPath}\"");
         }
         else if (OperatingSystem.IsLinux())
         {
             System.Diagnostics.Process.Start("xdg-open", Path.GetDirectoryName(map.FullPath) ?? map.FullPath);
         }
+        else if (OperatingSystem.IsMacOS())
+        {
+            System.Diagnostics.Process.Start("open", new[] { "-R", map.FullPath });
+        }
     }
     catch (Exception ex)
     {
         logger.LogError(ex, "Failed to reveal map: {FileName}", map.FileName);
     }
 }

Note: On macOS, open -R reveals the file in Finder (similar to Windows' /select).

GenHub/GenHub/Features/Tools/ReplayManager/Services/ReplayDirectoryService.cs (2)

114-128: Consider adding macOS support.

The implementation handles Windows and Linux platforms but doesn't provide support for macOS. macOS users would be unable to open directories using this feature.

Consider adding macOS support:

Suggested enhancement
 public void OpenDirectory(GameType version)
 {
     var path = GetReplayDirectory(version);
     if (Directory.Exists(path))
     {
         if (OperatingSystem.IsWindows())
         {
             Process.Start("explorer.exe", path);
         }
         else if (OperatingSystem.IsLinux())
         {
             Process.Start("xdg-open", path);
         }
+        else if (OperatingSystem.IsMacOS())
+        {
+            Process.Start("open", path);
+        }
     }
 }

131-146: Consider adding macOS support.

The implementation handles Windows and Linux platforms but doesn't provide support for macOS. macOS users would be unable to reveal files using this feature.

Consider adding macOS support:

Suggested enhancement
 public void RevealFile(ReplayFile replay)
 {
     if (File.Exists(replay.FullPath))
     {
         if (OperatingSystem.IsWindows())
         {
             Process.Start("explorer.exe", $"/select,\"{replay.FullPath}\"");
         }
         else if (OperatingSystem.IsLinux())
         {
             // Linux doesn't have a standard 'select' argument for file managers,
             // so we just open the directory containing the file.
             Process.Start("xdg-open", Path.GetDirectoryName(replay.FullPath) ?? replay.FullPath);
         }
+        else if (OperatingSystem.IsMacOS())
+        {
+            Process.Start("open", new[] { "-R", replay.FullPath });
+        }
     }
 }

Note: On macOS, open -R reveals the file in Finder (similar to Windows' /select).

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 55a9182 and 0d4b239.

📒 Files selected for processing (25)
  • GenHub/GenHub.Core/Constants/MapManagerConstants.cs
  • GenHub/GenHub.Core/Constants/ReplayManagerConstants.cs
  • GenHub/GenHub.Core/Constants/UiConstants.cs
  • GenHub/GenHub.Core/Interfaces/Tools/MapManager/IMapDirectoryService.cs
  • GenHub/GenHub.Core/Interfaces/Tools/MapManager/IMapPackService.cs
  • GenHub/GenHub.Core/Interfaces/Tools/ReplayManager/IReplayDirectoryService.cs
  • GenHub/GenHub.Core/Models/Tools/ToolMetadata.cs
  • GenHub/GenHub.Linux/GameInstallations/SteamInstallation.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Validation/FileSystemValidatorTests.cs
  • GenHub/GenHub.Windows/Program.cs
  • GenHub/GenHub/Features/Tools/MapManager/Services/MapDirectoryService.cs
  • GenHub/GenHub/Features/Tools/MapManager/Services/MapPackService.cs
  • GenHub/GenHub/Features/Tools/MapManager/Services/TgaParser.cs
  • GenHub/GenHub/Features/Tools/MapManager/ViewModels/MapManagerViewModel.cs
  • GenHub/GenHub/Features/Tools/MapManager/Views/MapManagerView.axaml
  • GenHub/GenHub/Features/Tools/ReplayManager/Services/ReplayDirectoryService.cs
  • GenHub/GenHub/Features/Tools/ReplayManager/ViewModels/ReplayManagerViewModel.cs
  • GenHub/GenHub/Features/Tools/ReplayManager/Views/ReplayManagerView.axaml
  • GenHub/GenHub/Features/Tools/ReplayManager/Views/ReplayManagerView.axaml.cs
  • GenHub/GenHub/Features/Tools/Services/UploadHistoryService.cs
  • GenHub/GenHub/Features/Tools/ViewModels/UploadHistoryItemViewModel.cs
  • GenHub/GenHub/Features/Validation/FileSystemValidator.cs
  • GenHub/GenHub/Infrastructure/Converters/BoolToTypeConverter.cs
  • GenHub/GenHub/Infrastructure/Converters/EnumToBoolConverter.cs
  • GenHub/GenHub/Infrastructure/Imaging/TgaImageParser.cs
💤 Files with no reviewable changes (1)
  • GenHub/GenHub.Windows/Program.cs
✅ Files skipped from review due to trivial changes (1)
  • GenHub/GenHub/Features/Tools/ReplayManager/Views/ReplayManagerView.axaml
🚧 Files skipped from review as they are similar to previous changes (1)
  • GenHub/GenHub/Features/Tools/MapManager/Services/TgaParser.cs
🧰 Additional context used
🧬 Code graph analysis (9)
GenHub/GenHub/Features/Validation/FileSystemValidator.cs (1)
GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Validation/FileSystemValidatorTests.cs (2)
  • Task (101-102)
  • Task (112-113)
GenHub/GenHub/Features/Tools/ReplayManager/Views/ReplayManagerView.axaml.cs (1)
GenHub/GenHub.Core/Constants/ReplayManagerConstants.cs (1)
  • ReplayManagerConstants (6-47)
GenHub/GenHub/Features/Tools/ViewModels/UploadHistoryItemViewModel.cs (1)
GenHub/GenHub.Core/Constants/UiConstants.cs (1)
  • UiConstants (6-101)
GenHub/GenHub/Features/Tools/MapManager/ViewModels/MapManagerViewModel.cs (1)
GenHub/GenHub/Features/Tools/MapManager/Services/MapDirectoryService.cs (2)
  • OpenDirectory (234-254)
  • RevealFile (257-274)
GenHub/GenHub/Features/Tools/ReplayManager/ViewModels/ReplayManagerViewModel.cs (2)
GenHub/GenHub/Features/Tools/ReplayManager/Services/ReplayDirectoryService.cs (2)
  • OpenDirectory (114-128)
  • RevealFile (131-146)
GenHub/GenHub.Core/Models/Tools/ReplayManager/ReplayFile.cs (1)
  • ReplayFile (9-52)
GenHub/GenHub/Infrastructure/Converters/EnumToBoolConverter.cs (2)
GenHub/GenHub/Infrastructure/Converters/BoolToTypeConverter.cs (1)
  • ConvertBack (25-28)
GenHub/GenHub/Infrastructure/Converters/EqualsToConverter.cs (1)
  • ConvertBack (25-28)
GenHub/GenHub/Infrastructure/Converters/BoolToTypeConverter.cs (1)
GenHub/GenHub.Core/Constants/MapManagerConstants.cs (1)
  • MapManagerConstants (6-112)
GenHub/GenHub/Features/Tools/MapManager/Services/MapPackService.cs (1)
GenHub/GenHub.Core/Constants/ManifestConstants.cs (1)
  • ManifestConstants (6-152)
GenHub/GenHub.Linux/GameInstallations/SteamInstallation.cs (2)
GenHub/GenHub.Core/Models/GameClients/GameClient.cs (1)
  • GameClient (8-107)
GenHub/GenHub.Core/Constants/GameClientConstants.cs (1)
  • GameClientConstants (8-244)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build Linux
  • GitHub Check: Build Windows
🔇 Additional comments (28)
GenHub/GenHub.Core/Interfaces/Tools/MapManager/IMapPackService.cs (1)

33-33: LGTM! Documentation typo fixed.

The correction from "repoter" to "reporter" improves the documentation quality.

GenHub/GenHub.Core/Models/Tools/ToolMetadata.cs (1)

19-24: LGTM!

The XML documentation comments now correctly describe their respective properties. This improves code clarity and maintainability.

GenHub/GenHub/Features/Validation/FileSystemValidator.cs (1)

35-35: LGTM! Clean refactoring from static to instance method.

The conversion of ValidateDirectoriesAsync from a static method to an instance method aligns well with the constructor-injected dependencies pattern used by this class. This change improves testability and follows best practices for dependency injection.

GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Validation/FileSystemValidatorTests.cs (1)

101-102: LGTM! Test properly adapted to instance method.

The test correctly adapts to the instance-based ValidateDirectoriesAsync by calling base.ValidateDirectoriesAsync instead of the previously static method. The use of the new keyword to expose the protected base method for testing is appropriate.

GenHub/GenHub/Features/Tools/ViewModels/UploadHistoryItemViewModel.cs (1)

3-3: LGTM! Good refactoring to centralized constants.

The change from hard-coded hex color values to UiConstants.StatusSuccessColor and UiConstants.StatusErrorColor improves maintainability and consistency across the UI. The using directive is correctly added to support these references.

Also applies to: 59-59

GenHub/GenHub.Core/Constants/UiConstants.cs (1)

28-31: LGTM! Consistent addition of accent color constant.

The new AccentColor constant is properly documented and follows the established pattern for color constants in this file. The placement before status colors is logical and maintains good organization.

GenHub/GenHub.Core/Constants/MapManagerConstants.cs (1)

23-31: LGTM! Well-documented display name constants.

The new constants are clearly documented and appropriately scoped. This centralizes display strings for better maintainability and potential future localization.

GenHub/GenHub/Infrastructure/Converters/BoolToTypeConverter.cs (1)

4-4: Excellent refactoring to eliminate magic strings.

Replacing hardcoded display strings with centralized constants improves maintainability and provides a single source of truth for these UI labels.

Also applies to: 18-18, 21-21

GenHub/GenHub.Linux/GameInstallations/SteamInstallation.cs (3)

55-55: LGTM: Modern collection expression syntax.

The change from new() to [] uses C# 12 collection expressions, making the initialization more concise while maintaining the same functionality.


130-136: LGTM: Explicit typing with collection expressions.

The explicit string[] type declaration combined with collection expression syntax improves code clarity while modernizing the syntax.


179-179: LGTM: Target-typed new expression.

Using target-typed new() reduces redundancy and aligns with modern C# conventions while maintaining the same functionality.

GenHub/GenHub/Features/Tools/MapManager/Services/MapPackService.cs (2)

44-48: LGTM: Improved variable naming.

The variable rename from subManager to subDirectory improves code clarity and accurately reflects the variable's purpose.


90-90: LGTM: Modern collection expression syntax.

The change from .ToList() to the collection expression [.. mapFilePaths] is a nice modernization using C# 12 syntax while preserving the same functionality.

GenHub/GenHub.Core/Constants/ReplayManagerConstants.cs (1)

32-46: LGTM! Well-documented constants centralize magic values.

The new constants (ReplayExtension, ZipExtension, RetentionDays) are appropriately named, documented, and follow existing conventions in the file. This centralization improves maintainability.

GenHub/GenHub/Features/Tools/ReplayManager/Views/ReplayManagerView.axaml.cs (3)

6-6: LGTM! Good addition to support constant usage.

The new using statement enables access to centralized constants, supporting the refactoring below.


59-61: LGTM! Replaces hard-coded extension with constant.

Using ReplayManagerConstants.ReplayExtension instead of the literal ".rep" improves maintainability and consistency.


99-100: LGTM! Centralizes file extension checks.

Replacing hard-coded ".rep" and ".zip" literals with ReplayManagerConstants.ReplayExtension and ReplayManagerConstants.ZipExtension eliminates magic strings and ensures consistency across the codebase.

GenHub/GenHub.Core/Interfaces/Tools/ReplayManager/IReplayDirectoryService.cs (1)

44-47: Method renames complete and consistent with MapManager interface.

The renames from OpenInExplorer/RevealInExplorer to OpenDirectory/RevealFile successfully align with IMapDirectoryService changes and create platform-agnostic terminology across the codebase. Implementation in ReplayDirectoryService (lines 114, 131) is complete, and all call sites have been properly updated—no remaining references to old method names exist in the codebase.

GenHub/GenHub.Core/Interfaces/Tools/MapManager/IMapDirectoryService.cs (1)

44-47: Method renames successfully improve cross-platform terminology.

The renames from OpenInExplorer/RevealInExplorer to OpenDirectory/RevealFile make the interface more platform-agnostic. The updated XML documentation correctly references "system file manager" instead of Windows-specific terminology. All implementations and call sites have been properly updated to use the new method names.

GenHub/GenHub/Features/Tools/ReplayManager/ViewModels/ReplayManagerViewModel.cs (4)

8-8: LGTM: Constants namespace added.

Good practice to centralize constants for maintainability.


534-535: Good refactoring: Hard-coded value replaced with constant.

Replacing the hard-coded 10 * 1024 * 1024 with ReplayManagerConstants.MaxUploadBytesPerPeriod improves maintainability and ensures consistency across the codebase.


608-608: LGTM: Method rename reflects interface modernization.

The rename from OpenInExplorer to OpenDirectory aligns with the updated interface.


614-614: LGTM: Method rename reflects interface modernization.

The rename from RevealInExplorer to RevealFile aligns with the updated interface.

GenHub/GenHub/Features/Tools/MapManager/ViewModels/MapManagerViewModel.cs (2)

655-655: LGTM: Method rename reflects interface modernization.

The rename from OpenInExplorer to OpenDirectory aligns with the updated interface.


661-661: LGTM: Method rename reflects interface modernization.

The rename from RevealInExplorer to RevealFile aligns with the updated interface.

GenHub/GenHub/Features/Tools/MapManager/Services/MapDirectoryService.cs (1)

130-130: LGTM: Modern collection expression syntax.

The use of [] for empty arrays is valid modern C# syntax and is more concise than new string[0] or Array.Empty<string>().

Also applies to: 162-162

GenHub/GenHub/Features/Tools/Services/UploadHistoryService.cs (2)

223-239: Good thread-safe pattern for async cleanup.

The implementation correctly takes a snapshot of pending deletions under lock and releases it before performing async I/O operations. This prevents deadlocks and ensures good performance.


247-247: No changes needed — C# 12 collection expressions with target-type inference are correctly applied.

Collection expressions like [] and [.. _cache] rely on target-type inference in C# 12. Since _cache is declared as List<UploadRecord>? and LoadHistoryInternal() returns List<UploadRecord>, the compiler correctly infers List<UploadRecord> for these expressions, not arrays. This is idiomatic C# 12 and type-safe. The original code pattern is correct.

var mapPack = new MapPack
{
Id = ManifestId.Create($"1.0.local.mappack.{name.ToLowerInvariant().Replace(" ", "-")}"),
Id = ManifestId.Create($"{ManifestConstants.DefaultManifestVersion}.0.local.mappack.{name.ToLowerInvariant().Replace(" ", "-")}"),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Incomplete input sanitization for manifest ID.

While using ManifestConstants.DefaultManifestVersion is good for consistency, the ID generation only sanitizes spaces but doesn't handle other special characters. Based on the PublisherContentRegexPattern from ManifestConstants.cs, the content name segment only allows [a-z0-9-]+. If the name parameter contains characters like parentheses, symbols, or other special characters, the generated ID will fail validation.

🔎 Proposed fix to sanitize all invalid characters
-            Id = ManifestId.Create($"{ManifestConstants.DefaultManifestVersion}.0.local.mappack.{name.ToLowerInvariant().Replace(" ", "-")}"),
+            Id = ManifestId.Create($"{ManifestConstants.DefaultManifestVersion}.0.local.mappack.{System.Text.RegularExpressions.Regex.Replace(name.ToLowerInvariant(), "[^a-z0-9-]", "-").Trim('-')}"),

This replaces any character that isn't a lowercase letter, number, or hyphen with a hyphen, and trims leading/trailing hyphens.

🤖 Prompt for AI Agents
In @GenHub/GenHub/Features/Tools/MapManager/Services/MapPackService.cs around
line 87, The manifest ID generation using ManifestId.Create with
ManifestConstants.DefaultManifestVersion currently only replaces spaces and can
produce invalid IDs when name contains other special characters; update the code
that builds the ID (the expression that uses name.ToLowerInvariant().Replace("
", "-")) to fully sanitize the content segment to match
PublisherContentRegexPattern by: lowercasing the name, replacing any character
not matching [a-z0-9-] with a hyphen, collapsing consecutive hyphens into one,
trimming leading/trailing hyphens, and ensuring a non-empty fallback (e.g.,
"unknown") before calling ManifestId.Create so the generated ID always
validates.

Comment on lines 62 to 66
// Initialization logic for the service
private async Task InitializeInternalAsync()
{
await RunCleanupAsync();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -nP --type=cs 'InitializeInternalAsync\s*\(' -C3

Repository: undead2146/GenHub

Length of output: 660


🏁 Script executed:

rg -nP --type=cs 'class UploadHistoryService|public UploadHistoryService\s*\(' -A10 GenHub/GenHub/Features/Tools/Services/UploadHistoryService.cs

Repository: undead2146/GenHub

Length of output: 493


🏁 Script executed:

rg -nP --type=cs 'interface IUploadHistoryService' -A20

Repository: undead2146/GenHub

Length of output: 2164


🏁 Script executed:

rg -nP --type=cs 'interface IUploadHistoryService' -A50 | head -60

Repository: undead2146/GenHub

Length of output: 4779


Remove the unused InitializeInternalAsync method or add a call site.

The InitializeInternalAsync method (lines 62-66) is defined but never invoked anywhere in the codebase. It does not appear in the IUploadHistoryService interface and is not called from the constructor or any other location. If this method is intended to handle initialization after the fire-and-forget cleanup was removed, ensure it is explicitly called during service startup; otherwise, remove it as dead code.

🤖 Prompt for AI Agents
In @GenHub/GenHub/Features/Tools/Services/UploadHistoryService.cs around lines
62-66, The private method InitializeInternalAsync currently only calls
RunCleanupAsync but is never used; either remove this dead method or wire it
into startup: add it to the IUploadHistoryService contract and call
InitializeInternalAsync from the service constructor (or the service
registration/startup routine) so initialization runs, or simply delete the
InitializeInternalAsync method and keep its logic where RunCleanupAsync is
invoked; reference InitializeInternalAsync and RunCleanupAsync when making the
change.

Comment on lines 98 to 101
if (srcIndex + (sourceBytesPerPixel == 4 ? 3 : 2) >= sourceData.Length)
{
return rgbaData;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Silent failure breaks error-handling conventions and hinders debugging.

The bounds check correctly prevents an IndexOutOfRangeException, but returning partially filled rgbaData (with zeros for incomplete pixels) silently produces a corrupted image. This diverges from the established pattern in this file where errors are logged and null is returned (see lines 26-27, 207-208, 213-214, 219-220, 248-249).

When sourceData is truncated—whether from a corrupt file or a bug in DecompressRle—the caller receives no indication of failure, making debugging difficult and potentially displaying a partially black image to users.

🔎 Recommended fix: log the issue and return null
     for (int i = 0; i < pixelCount; i++)
     {
         var srcIndex = i * sourceBytesPerPixel;
         var dstIndex = i * 4;

-        if (srcIndex + (sourceBytesPerPixel == 4 ? 3 : 2) >= sourceData.Length)
-        {
-            return rgbaData;
-        }
+        if (srcIndex + (sourceBytesPerPixel == 4 ? 3 : 2) >= sourceData.Length)
+        {
+            logger.LogWarning("Insufficient source data for pixel {Pixel}/{Total}: expected {Expected} bytes, got {Actual} bytes",
+                i, pixelCount, pixelCount * sourceBytesPerPixel, sourceData.Length);
+            return null;
+        }

         rgbaData[dstIndex] = sourceData[srcIndex + 2];

Note: This will require updating the method signature to return byte[]? and handling the null return in ParseTgaFile at line 236:

var rgbaData = ConvertToRgba(imageData, width, height, bytesPerPixel);
if (rgbaData == null)
{
    logger.LogWarning("Failed to convert TGA data to RGBA: {Path}", path);
    return null;
}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @GenHub/GenHub/Infrastructure/Imaging/TgaImageParser.cs around lines 98-101,
The bounds-check in ConvertToRgba currently returns a partially filled rgbaData
on truncated sourceData; change ConvertToRgba's signature to return byte[]? and,
instead of returning rgbaData on that check, log a warning via the class logger
(include path/context if available) and return null; then update the caller
ParseTgaFile to handle a null return (check the result, log "Failed to convert
TGA data to RGBA: {Path}" or similar, and propagate null) so
truncated/decompress errors are surfaced rather than producing corrupted images.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
GenHub/GenHub/Features/Tools/MapManager/Views/MapManagerView.axaml (1)

204-204: Verify consistency between "per file" and "per map" terminology.

Line 204 displays "(max 10MB per file)" while the footer at lines 282-283 states "Maximum 10MB per map." Ensure these messages use consistent terminology—either "per file" or "per map"—to avoid user confusion.

Also applies to: 282-283

🤖 Fix all issues with AI Agents
In
@GenHub/GenHub/Features/Tools/ReplayManager/Services/ReplayDirectoryService.cs:
- Around line 131-146: The RevealFile method currently calls Process.Start
without error handling; wrap the Windows and Linux Process.Start calls in a
try-catch, mirror the pattern used in MapDirectoryService/OpenDirectory: catch
exceptions, log the error via the same logger (use the
ReplayDirectoryService/RevealFile context), and avoid throwing so the UI won't
crash; ensure you still check File.Exists and fall back to opening the directory
when Path.GetDirectoryName returns null.
🧹 Nitpick comments (5)
GenHub/GenHub/Features/Tools/ReplayManager/ViewModels/ReplayManagerViewModel.cs (1)

537-542: Consider referencing constant value in error message.

The error message on lines 538-540 displays "Maximum upload size is 10MB" as a hard-coded string. Consider dynamically referencing ReplayManagerConstants.MaxUploadBytesPerPeriod to keep the message in sync with the actual limit:

$"File too large. Maximum upload size is {ReplayManagerConstants.MaxUploadBytesPerPeriod / (1024 * 1024)}MB."

This ensures the message automatically updates if the constant changes.

🔎 Proposed refactor
         notificationService.ShowError(
            "File Too Large",
-           "File too large. Maximum upload size is 10MB.");
+           $"File too large. Maximum upload size is {ReplayManagerConstants.MaxUploadBytesPerPeriod / (1024 * 1024)}MB.");
-       StatusMessage = "Upload too large (Max 10MB).";
+       StatusMessage = $"Upload too large (Max {ReplayManagerConstants.MaxUploadBytesPerPeriod / (1024 * 1024)}MB).";
GenHub/GenHub/Features/Tools/ReplayManager/Services/ReplayDirectoryService.cs (1)

119-126: Consider adding macOS support.

Both methods support Windows and Linux but lack macOS support. On macOS, you can use the open command similarly to Linux's xdg-open. For RevealFile, use open -R to reveal/select the file.

Example implementation

For OpenDirectory:

else if (OperatingSystem.IsMacOS())
{
    Process.Start("open", path);
}

For RevealFile:

else if (OperatingSystem.IsMacOS())
{
    Process.Start("open", $"-R \"{replay.FullPath}\"");
}

Also applies to: 135-144

GenHub/GenHub/Features/Tools/MapManager/Services/MapDirectoryService.cs (2)

234-254: Well-implemented with proper error handling.

The method correctly implements cross-platform support with appropriate error handling and logging. This serves as a good pattern that should be replicated in ReplayDirectoryService.

Consider adding macOS support using OperatingSystem.IsMacOS() and the open command for completeness:

else if (OperatingSystem.IsMacOS())
{
    System.Diagnostics.Process.Start("open", directory);
}

257-274: Well-implemented with proper error handling.

The method correctly implements cross-platform file revealing with appropriate error handling and logging.

Consider adding macOS support for file selection using the open -R command:

else if (OperatingSystem.IsMacOS())
{
    System.Diagnostics.Process.Start("open", $"-R \"{map.FullPath}\"");
}
GenHub/GenHub/Features/GameProfiles/ViewModels/GameProfileItemViewModel.cs (1)

408-420: Property usage is correct; consider extracting version validation.

The switch to using GameVersion and Publisher properties improves consistency. The logic correctly filters special version strings from the display.

Optional: Extract duplicated version validation logic

The version string checks (lines 382-385, 408-412) are duplicated. Consider extracting to a helper method:

private static bool IsSpecialVersionString(string? version)
{
    return string.IsNullOrEmpty(version) ||
           version.Equals(GameClientConstants.AutoDetectedVersion, StringComparison.OrdinalIgnoreCase) ||
           version.Equals("Unknown", StringComparison.OrdinalIgnoreCase) ||
           version.Equals("Auto-Updated", StringComparison.OrdinalIgnoreCase) ||
           version.Contains("Automatically", StringComparison.OrdinalIgnoreCase);
}

Then use: if (IsSpecialVersionString(version)) and if (!IsSpecialVersionString(GameVersion)).

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0d4b239 and 296386f.

📒 Files selected for processing (27)
  • GenHub/GenHub.Core/Constants/MapManagerConstants.cs
  • GenHub/GenHub.Core/Constants/ReplayManagerConstants.cs
  • GenHub/GenHub.Core/Constants/UiConstants.cs
  • GenHub/GenHub.Core/Interfaces/Tools/MapManager/IMapDirectoryService.cs
  • GenHub/GenHub.Core/Interfaces/Tools/MapManager/IMapPackService.cs
  • GenHub/GenHub.Core/Interfaces/Tools/ReplayManager/IReplayDirectoryService.cs
  • GenHub/GenHub.Core/Models/Tools/ToolMetadata.cs
  • GenHub/GenHub.Linux/GameInstallations/SteamInstallation.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Validation/FileSystemValidatorTests.cs
  • GenHub/GenHub.Windows/Program.cs
  • GenHub/GenHub/Features/Content/Services/GeneralsOnline/GeneralsOnlineProfileReconciler.cs
  • GenHub/GenHub/Features/GameProfiles/ViewModels/GameProfileItemViewModel.cs
  • GenHub/GenHub/Features/Tools/MapManager/Services/MapDirectoryService.cs
  • GenHub/GenHub/Features/Tools/MapManager/Services/MapPackService.cs
  • GenHub/GenHub/Features/Tools/MapManager/Services/TgaParser.cs
  • GenHub/GenHub/Features/Tools/MapManager/ViewModels/MapManagerViewModel.cs
  • GenHub/GenHub/Features/Tools/MapManager/Views/MapManagerView.axaml
  • GenHub/GenHub/Features/Tools/ReplayManager/Services/ReplayDirectoryService.cs
  • GenHub/GenHub/Features/Tools/ReplayManager/ViewModels/ReplayManagerViewModel.cs
  • GenHub/GenHub/Features/Tools/ReplayManager/Views/ReplayManagerView.axaml
  • GenHub/GenHub/Features/Tools/ReplayManager/Views/ReplayManagerView.axaml.cs
  • GenHub/GenHub/Features/Tools/Services/UploadHistoryService.cs
  • GenHub/GenHub/Features/Tools/ViewModels/UploadHistoryItemViewModel.cs
  • GenHub/GenHub/Features/Validation/FileSystemValidator.cs
  • GenHub/GenHub/Infrastructure/Converters/BoolToTypeConverter.cs
  • GenHub/GenHub/Infrastructure/Converters/EnumToBoolConverter.cs
  • GenHub/GenHub/Infrastructure/Imaging/TgaImageParser.cs
💤 Files with no reviewable changes (1)
  • GenHub/GenHub.Windows/Program.cs
✅ Files skipped from review due to trivial changes (2)
  • GenHub/GenHub/Features/Content/Services/GeneralsOnline/GeneralsOnlineProfileReconciler.cs
  • GenHub/GenHub.Core/Models/Tools/ToolMetadata.cs
🚧 Files skipped from review as they are similar to previous changes (14)
  • GenHub/GenHub/Features/Tools/ViewModels/UploadHistoryItemViewModel.cs
  • GenHub/GenHub/Features/Tools/Services/UploadHistoryService.cs
  • GenHub/GenHub/Features/Tools/MapManager/ViewModels/MapManagerViewModel.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Validation/FileSystemValidatorTests.cs
  • GenHub/GenHub.Core/Interfaces/Tools/MapManager/IMapDirectoryService.cs
  • GenHub/GenHub/Infrastructure/Converters/BoolToTypeConverter.cs
  • GenHub/GenHub/Features/Tools/MapManager/Services/MapPackService.cs
  • GenHub/GenHub.Core/Interfaces/Tools/MapManager/IMapPackService.cs
  • GenHub/GenHub.Core/Constants/ReplayManagerConstants.cs
  • GenHub/GenHub/Infrastructure/Converters/EnumToBoolConverter.cs
  • GenHub/GenHub.Core/Constants/MapManagerConstants.cs
  • GenHub/GenHub/Infrastructure/Imaging/TgaImageParser.cs
  • GenHub/GenHub/Features/Tools/MapManager/Services/TgaParser.cs
  • GenHub/GenHub.Linux/GameInstallations/SteamInstallation.cs
🧰 Additional context used
🧬 Code graph analysis (3)
GenHub/GenHub/Features/Tools/ReplayManager/ViewModels/ReplayManagerViewModel.cs (3)
GenHub/GenHub.Core/Constants/ReplayManagerConstants.cs (1)
  • ReplayManagerConstants (6-47)
GenHub/GenHub.Core/Interfaces/Tools/ReplayManager/IReplayDirectoryService.cs (2)
  • OpenDirectory (47-47)
  • RevealFile (53-53)
GenHub/GenHub/Features/Tools/ReplayManager/Services/ReplayDirectoryService.cs (2)
  • OpenDirectory (114-128)
  • RevealFile (131-146)
GenHub/GenHub/Features/Tools/ReplayManager/Views/ReplayManagerView.axaml.cs (1)
GenHub/GenHub.Core/Constants/ReplayManagerConstants.cs (1)
  • ReplayManagerConstants (6-47)
GenHub/GenHub/Features/Tools/ReplayManager/Services/ReplayDirectoryService.cs (3)
GenHub/GenHub.Core/Interfaces/Tools/MapManager/IMapDirectoryService.cs (2)
  • OpenDirectory (47-47)
  • RevealFile (53-53)
GenHub/GenHub.Core/Interfaces/Tools/ReplayManager/IReplayDirectoryService.cs (2)
  • OpenDirectory (47-47)
  • RevealFile (53-53)
GenHub/GenHub.Core/Models/Tools/ReplayManager/ReplayFile.cs (1)
  • ReplayFile (9-52)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build Windows
  • GitHub Check: Build Linux
🔇 Additional comments (9)
GenHub/GenHub/Features/Validation/FileSystemValidator.cs (1)

35-35: LGTM! Improved consistency by converting to instance method.

Converting ValidateDirectoriesAsync from static to instance method aligns with the pattern used by ValidateFilesAsync (line 70) and improves consistency within the class. While the method doesn't currently access instance members, this change provides flexibility to use _logger or _hashProvider in the future without requiring breaking changes to derived classes.

GenHub/GenHub.Core/Constants/UiConstants.cs (1)

28-31: LGTM! Global accent color added correctly.

The AccentColor constant is properly implemented with a valid hex color value and clear documentation. The placement before status colors is appropriate for a global UI accent color.

GenHub/GenHub/Features/Tools/ReplayManager/Views/ReplayManagerView.axaml.cs (1)

6-6: LGTM! Constants usage improves maintainability.

The refactoring to use ReplayManagerConstants.ReplayExtension and ReplayManagerConstants.ZipExtension centralizes file extension handling and improves consistency across the codebase.

Also applies to: 59-61, 99-100

GenHub/GenHub/Features/Tools/ReplayManager/Views/ReplayManagerView.axaml (1)

183-183: LGTM! File size messaging aligns with constants.

The updated text correctly reflects the size limits defined in ReplayManagerConstants: 1MB per replay file and 10MB total per 3-day period. The messaging is clear and consistent.

Also applies to: 261-262

GenHub/GenHub/Features/Tools/ReplayManager/ViewModels/ReplayManagerViewModel.cs (2)

8-8: LGTM! Constants usage improves maintainability.

The refactoring to use ReplayManagerConstants.MaxUploadBytesPerPeriod centralizes the size limit configuration and ensures consistency across the codebase.

Also applies to: 534-535


608-608: LGTM! Method renames improve cross-platform clarity.

The method renames from OpenInExplorer/RevealInExplorer to OpenDirectory/RevealFile make the API more platform-agnostic and better reflect their behavior on non-Windows systems.

Also applies to: 614-614

GenHub/GenHub.Core/Interfaces/Tools/ReplayManager/IReplayDirectoryService.cs (1)

44-47: LGTM! Interface method renames improve cross-platform clarity.

The method renames from OpenInExplorer/RevealInExplorer to OpenDirectory/RevealFile better reflect platform-agnostic behavior. The updated XML documentation appropriately references "system file manager" terminology.

Also applies to: 50-53

GenHub/GenHub/Features/Tools/MapManager/Services/MapDirectoryService.cs (1)

86-90: No action needed. The AssetFiles property is typed as List<string> (not string[]), so line 90's .ToList() is correct and matches the property type. All assignments at lines 109, 130, and 162 are type-consistent.

Likely an incorrect or invalid review comment.

GenHub/GenHub/Features/GameProfiles/ViewModels/GameProfileItemViewModel.cs (1)

378-393: Correct usage of the generated GameVersion property.

The refactoring from the private backing field _gameVersion to the public observable property GameVersion is correct and follows MVVM best practices. PropertyChanged notifications fired during construction are harmless since no subscribers exist yet.

Comment on lines 114 to 128
public void OpenDirectory(GameType version)
{
var path = GetReplayDirectory(version);
if (Directory.Exists(path))
{
Process.Start(new ProcessStartInfo
if (OperatingSystem.IsWindows())
{
FileName = PlatformConstants.WindowsExplorerExecutable,
Arguments = path,
UseShellExecute = true,
});
Process.Start("explorer.exe", path);
}
else if (OperatingSystem.IsLinux())
{
Process.Start("xdg-open", path);
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Add error handling to prevent unhandled exceptions.

The Process.Start calls can throw exceptions (e.g., Win32Exception, FileNotFoundException, ObjectDisposedException) that will propagate to callers. MapDirectoryService implements the same logic with proper try-catch blocks and error logging (lines 239-253). Apply the same pattern here for consistency and robustness.

🔎 Proposed fix with error handling
 public void OpenDirectory(GameType version)
 {
     var path = GetReplayDirectory(version);
-    if (Directory.Exists(path))
+    EnsureDirectoryExists(version);
+
+    try
     {
         if (OperatingSystem.IsWindows())
         {
             Process.Start("explorer.exe", path);
         }
         else if (OperatingSystem.IsLinux())
         {
             Process.Start("xdg-open", path);
         }
     }
+    catch (Exception ex)
+    {
+        logger.LogError(ex, "Failed to open replay directory: {Directory}", path);
+    }
 }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 131 to 146
public void RevealFile(ReplayFile replay)
{
if (File.Exists(replay.FullPath))
{
Process.Start(new ProcessStartInfo
if (OperatingSystem.IsWindows())
{
Process.Start("explorer.exe", $"/select,\"{replay.FullPath}\"");
}
else if (OperatingSystem.IsLinux())
{
FileName = PlatformConstants.WindowsExplorerExecutable,
Arguments = string.Format(PlatformConstants.WindowsExplorerSelectArgument, replay.FullPath),
UseShellExecute = true,
});
// Linux doesn't have a standard 'select' argument for file managers,
// so we just open the directory containing the file.
Process.Start("xdg-open", Path.GetDirectoryName(replay.FullPath) ?? replay.FullPath);
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Add error handling to prevent unhandled exceptions.

Similar to OpenDirectory, the Process.Start calls lack error handling. MapDirectoryService implements identical logic with try-catch blocks and error logging (lines 257-274). Apply the same pattern for consistency.

🔎 Proposed fix with error handling
 public void RevealFile(ReplayFile replay)
 {
-    if (File.Exists(replay.FullPath))
+    try
     {
         if (OperatingSystem.IsWindows())
         {
             Process.Start("explorer.exe", $"/select,\"{replay.FullPath}\"");
         }
         else if (OperatingSystem.IsLinux())
         {
-            // Linux doesn't have a standard 'select' argument for file managers,
-            // so we just open the directory containing the file.
             Process.Start("xdg-open", Path.GetDirectoryName(replay.FullPath) ?? replay.FullPath);
         }
     }
+    catch (Exception ex)
+    {
+        logger.LogError(ex, "Failed to reveal replay: {FileName}", replay.FileName);
+    }
 }
📝 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
public void RevealFile(ReplayFile replay)
{
if (File.Exists(replay.FullPath))
{
Process.Start(new ProcessStartInfo
if (OperatingSystem.IsWindows())
{
Process.Start("explorer.exe", $"/select,\"{replay.FullPath}\"");
}
else if (OperatingSystem.IsLinux())
{
FileName = PlatformConstants.WindowsExplorerExecutable,
Arguments = string.Format(PlatformConstants.WindowsExplorerSelectArgument, replay.FullPath),
UseShellExecute = true,
});
// Linux doesn't have a standard 'select' argument for file managers,
// so we just open the directory containing the file.
Process.Start("xdg-open", Path.GetDirectoryName(replay.FullPath) ?? replay.FullPath);
}
}
}
public void RevealFile(ReplayFile replay)
{
try
{
if (File.Exists(replay.FullPath))
{
if (OperatingSystem.IsWindows())
{
Process.Start("explorer.exe", $"/select,\"{replay.FullPath}\"");
}
else if (OperatingSystem.IsLinux())
{
// Linux doesn't have a standard 'select' argument for file managers,
// so we just open the directory containing the file.
Process.Start("xdg-open", Path.GetDirectoryName(replay.FullPath) ?? replay.FullPath);
}
}
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to reveal replay: {FileName}", replay.FileName);
}
}
🤖 Prompt for AI Agents
In
@GenHub/GenHub/Features/Tools/ReplayManager/Services/ReplayDirectoryService.cs
around lines 131-146, The RevealFile method currently calls Process.Start
without error handling; wrap the Windows and Linux Process.Start calls in a
try-catch, mirror the pattern used in MapDirectoryService/OpenDirectory: catch
exceptions, log the error via the same logger (use the
ReplayDirectoryService/RevealFile context), and avoid throwing so the UI won't
crash; ensure you still check File.Exists and fall back to opening the directory
when Path.GetDirectoryName returns null.

@undead2146 undead2146 force-pushed the feat/map-manager branch 2 times, most recently from 2bb290d to 90ef3d1 Compare January 6, 2026 07:25
undead2146 and others added 6 commits January 6, 2026 08:45
…port and GitHub rate limit tracking (community-outpost#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.
…le 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.
undead2146 and others added 4 commits January 7, 2026 23:06
…y launching (community-outpost#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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants