Skip to content

Allow live content hotswapping on running game profiles for non-binary content types #370

Description

@undead2146

Overview

Users cannot edit a game profile while the game is running. The sole gate is GameProfileItemViewModel.CanEdit:

If a user wants to add a custom map while sitting in a lobby, they must quit the game, edit the profile, wait for workspace rebuild, and relaunch. For maps specifically this is entirely unnecessary — the C&C Generals / Zero Hour engine reads maps from the user's Documents folder (Documents\Command and Conquer Generals Zero Hour Data\Maps\) at runtime, meaning new maps can appear in the game's map list without restarting.

Indiscriminate editing is unsafe: modifying binaries or foundation assets (GameClient, GameInstallation, Mod, Addon) while the process holds file handles will cause ERROR_SHARING_VIOLATION crashes, corrupted workspaces, or failed reconciliation. A selective approach is needed.

Motivating use case

A user is in a Zero Hour lobby and realises they need a specific custom map. Today they must:

  1. Alt-Tab → quit the game
  2. Open GenHub → edit the profile → enable the map
  3. Wait for workspace rebuild
  4. Relaunch the game

With hotswapping, the flow becomes:

  1. Alt-Tab → open GenHub → edit the profile → enable the map → save
  2. The map appears in Documents\...\Maps\ immediately via hard link from CAS
  3. Alt-Tab back → the map is visible in the game's map list

Key Architecture Discovery: Maps Use User Data, Not the Workspace

Maps do not go to the workspace directory. They are deployed to the user's Documents folder via a completely separate pipeline:

ContentInstallTarget.UserMapsDirectory
  → Documents\Command and Conquer Generals Zero Hour Data\Maps\

The deployment pipeline is:

  • IUserDataTracker (IUserDataTracker.cs) — tracks installed user data files, creates hard links from CAS to Documents, backs up overwritten files, and restores them on uninstall.
  • ProfileContentLinkerService (ProfileContentLinkerService.cs) — orchestrates per-profile user data lifecycle. Has delta logic for install/uninstall.
  • GameLauncher (GameLauncher.cs:764-801) — calls SwitchProfileUserDataAsync at launch time to link maps into the Documents folder.

Critical finding: ProfileContentLinkerService.UpdateProfileUserDataAsync (lines 232-301) already implements the exact delta logic needed for hotswapping:

  • Computes which manifests to add (in new set but not current)
  • Computes which manifests to remove (in current but not new set)
  • Calls userDataTracker.InstallUserDataAsync for additions
  • Calls userDataTracker.UninstallUserDataAsync for removals (which restores backed-up files — the operation is reversible)
  • Re-activates if the profile is the active profile

However, UpdateProfileUserDataAsync has zero callers in production code. The infrastructure exists but is never wired into the profile save path. The hotswap implementation for maps is primarily about connecting this existing method into the save flow when a game is running.

The workspace reconciliation path (WorkspaceManager / WorkspaceReconciler) is irrelevant for maps and should not be touched for this feature.

Current Architecture Gaps

  1. GameProfileManager has zero awareness of launch state. It does not depend on ILaunchRegistry. UpdateProfileAsync (GameProfileManager.cs:217-240) unconditionally clears ActiveWorkspaceId when EnabledContentIds changes — this would corrupt workspace tracking for a running game.

  2. GameProfileSettingsViewModel has no concept of a running profile. Its constructor takes no ILaunchRegistry dependency. SaveAsync (GameProfileSettingsViewModel.Commands.cs:296-453) calls _gameProfileManager.UpdateProfileAsync with no process checks and no call to ProfileContentLinkerService.UpdateProfileUserDataAsync.

  3. ProfileContentLinkerService.UpdateProfileUserDataAsync is dead code. The method exists with correct delta logic but is never called. Maps only get deployed at launch time via SwitchProfileUserDataAsync in GameLauncher.

  4. ContentReconciliationService can destroy running workspaces. InvalidateWorkspacesForManifestInternalAsync (ContentReconciliationService.cs:386-422) iterates profiles and calls workspaceManager.CleanupWorkspaceAsync + clears ActiveWorkspaceId without checking ILaunchRegistry. A background content update can delete a workspace underneath a running game.

  5. IGameProfile is a pure data model. Runtime state (IsRunning) lives only in LaunchRegistry (via GameLaunchInfo.IsRunning) and the view model layer (GameProfileItemViewModel.IsProcessRunning). There is no service-level guard.

Requirements

Hotswap eligibility classification

Content types from ContentType.cs must be classified into two operational classes during active game sessions. This classification should follow the same constant pattern as ContentTypePriority (GenHub.Core.Models.Workspace):

Hotswappable (deployed to user Documents via IUserDataTracker, read dynamically by the engine):

  • Map, MapPack — deployed to ContentInstallTarget.UserMapsDirectory

Not yet hotswappable (deployed to workspace, no Documents-based loading path exists yet):

  • Patch, Skin, LanguagePack, Mission — these use ContentInstallTarget.Workspace and would require workspace modification while the game holds file locks. Out of scope for the initial implementation.

Locked (loaded into process memory at launch, unsafe to modify):

  • GameInstallation, GameClient, Mod, Addon, Executable, ModdingTool, UnknownContentType

Immutable profile metadata (structural properties that define workspace root or launch target):

  • GameInstallationId, GameClient, WorkspaceStrategy, CustomExecutablePath, WorkingDirectory

Functional requirements

  1. A user must be able to open the profile editor while a profile is running.
  2. Locked content types, workspace-targeted content types, and immutable profile metadata must be visibly disabled in the editor UI with an explanatory indicator when the profile is running.
  3. Saving a running profile with only map/mappack content changes must deploy (or remove) the maps to the user's Documents folder via ProfileContentLinkerService.UpdateProfileUserDataAsync without clearing ActiveWorkspaceId and without disrupting the running game process.
  4. Map removal must be reversible — IUserDataTracker.UninstallUserDataAsync must restore any backed-up files that were overwritten during installation.
  5. Saving a running profile with any locked content type change, workspace-targeted content change, or immutable metadata change must be rejected with a descriptive, user-facing error — profile state must not be mutated.
  6. Background content reconciliation (ContentReconciliationService) must not invalidate or delete workspaces for profiles that are currently running.

Proposed Approach

1. Add ContentHotswapClassification constants

Create a static class alongside ContentTypePriority in GenHub.Core.Models.Workspace (or GenHub.Core.Constants) that provides:

  • IsHotswappable(ContentType) — returns true for Map and MapPack only (initial scope)
  • IsLocked(ContentType) — returns true for GameInstallation, GameClient, Mod, Addon, Executable, ModdingTool

2. Inject ILaunchRegistry into GameProfileManager

Give GameProfileManager awareness of running state by adding ILaunchRegistry as a constructor dependency. In UpdateProfileAsync:

  • Query ILaunchRegistry.GetAllActiveLaunchesAsync() to determine if the target profile is running.
  • If running and content changed:
    • Resolve added/removed manifests via IContentManifestPool.
    • Validate every changed manifest is hotswappable using ContentHotswapClassification.IsHotswappable.
    • Validate no immutable profile metadata changed.
    • If validation passes: skip the ActiveWorkspaceId = string.Empty line at GameProfileManager.cs:239. Persist the profile update normally.
    • If validation fails: return ProfileOperationResult.CreateFailure with a descriptive message and do not mutate profile state.
  • If not running: proceed with existing behaviour (clear ActiveWorkspaceId, rebuild on next launch).

3. Wire UpdateProfileUserDataAsync into the save path

This is the core of the feature. When GameProfileSettingsViewModel.SaveAsync saves a running profile with hotswappable content changes:

  • After _gameProfileManager.UpdateProfileAsync succeeds, resolve the full manifests for the new EnabledContentIds.
  • Call ProfileContentLinkerService.UpdateProfileUserDataAsync(profileId, newManifests, targetGame).
  • This method already handles the complete delta: it installs new map hard links from CAS to Documents\...\Maps\ and uninstalls removed maps with backup restoration.
  • Surface the result as a toast notification: success → "Maps updated live", failure → warning with error detail.

The GameProfileSettingsViewModel will need IProfileContentLinker and ILaunchRegistry (or a derived boolean) as new dependencies.

4. Guard ContentReconciliationService against running profiles

In InvalidateWorkspacesForManifestInternalAsync and ReconcileBulkManifestReplacementInternalAsync:

  • Query ILaunchRegistry before calling workspaceManager.CleanupWorkspaceAsync.
  • If the profile is running: skip workspace cleanup and log a warning. The existing ActiveWorkspaceId mismatch detection on next launch will handle deferred reconciliation.
  • If the profile is not running: proceed with existing behaviour.

5. Propagate running state to the editor UI

  • Add ILaunchRegistry (or a derived bool IsProfileRunning) as a dependency to GameProfileSettingsViewModel.
  • When the editor is opened for a running profile:
    • Display a prominent "Hotswap Mode" status indicator.
    • Disable interactive elements for locked content types (GameClient, GameInstallation, Mod, Addon) and workspace-targeted types (Patch, Skin, LanguagePack) and immutable metadata fields (WorkspaceStrategy, GameInstallation dropdown) with explanatory tooltips.
    • Keep checkboxes enabled only for Map and MapPack.
  • Update GameProfileItemViewModel.CanEdit:
    public bool CanEdit => !IsPreparingWorkspace;

Acceptance Criteria

  • A ContentHotswapClassification (or equivalent constant class) exists with an IsHotswappable(ContentType) method that returns true for Map and MapPack.
  • GameProfileManager depends on ILaunchRegistry and checks profile running state in UpdateProfileAsync before deciding on workspace invalidation.
  • GameProfileItemViewModel.CanEdit permits opening the profile editor while a profile is running.
  • GameProfileSettingsViewModel dynamically disables locked fields and workspace-targeted content types, and displays a "Hotswap Mode" indicator when the profile is running. Only Map/MapPack checkboxes remain enabled.
  • Saving a running profile with a newly enabled Map or MapPack calls ProfileContentLinkerService.UpdateProfileUserDataAsync, which installs the map files as hard links from CAS to Documents\Command and Conquer Generals Zero Hour Data\Maps\.
  • The newly installed map is visible in the game's map list without restarting the game process.
  • Saving a running profile with a removed Map or MapPack calls IUserDataTracker.UninstallUserDataAsync, which removes the hard links and restores any backed-up files.
  • Saving a running profile with any locked or workspace-targeted content type change or immutable metadata change returns a descriptive error without mutating profile state.
  • ContentReconciliationService does not call CleanupWorkspaceAsync or clear ActiveWorkspaceId on profiles that are currently running.
  • Unit tests cover: hotswap classification for all ContentType values, running-profile update validation (accept Map/MapPack, reject locked types), live user data deployment via UpdateProfileUserDataAsync, map removal with backup restoration, and ContentReconciliationService running-profile guard.

Explicitly Out of Scope

  • Live modification of GameClient executables, GameInstallation archives, Mod BIG archives, or Addon DLLs while the game is running.
  • Hotswapping Patch, Skin, or LanguagePack content — these deploy to the workspace, not the user's Documents folder, and no Documents-based loading path exists for them yet.
  • In-memory code injection or live DLL hot-patching.
  • Automatic reconciliation of hotswapped content when the game process exits (tracked separately if needed).

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions