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:
- Alt-Tab → quit the game
- Open GenHub → edit the profile → enable the map
- Wait for workspace rebuild
- Relaunch the game
With hotswapping, the flow becomes:
- Alt-Tab → open GenHub → edit the profile → enable the map → save
- The map appears in
Documents\...\Maps\ immediately via hard link from CAS
- 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
-
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.
-
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.
-
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.
-
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.
-
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
- A user must be able to open the profile editor while a profile is running.
- 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.
- 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.
- Map removal must be reversible —
IUserDataTracker.UninstallUserDataAsync must restore any backed-up files that were overwritten during installation.
- 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.
- 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
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).
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 causeERROR_SHARING_VIOLATIONcrashes, 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:
With hotswapping, the flow becomes:
Documents\...\Maps\immediately via hard link from CASKey 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:
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) — callsSwitchProfileUserDataAsyncat 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:userDataTracker.InstallUserDataAsyncfor additionsuserDataTracker.UninstallUserDataAsyncfor removals (which restores backed-up files — the operation is reversible)However,
UpdateProfileUserDataAsynchas 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
GameProfileManagerhas zero awareness of launch state. It does not depend onILaunchRegistry.UpdateProfileAsync(GameProfileManager.cs:217-240) unconditionally clearsActiveWorkspaceIdwhenEnabledContentIdschanges — this would corrupt workspace tracking for a running game.GameProfileSettingsViewModelhas no concept of a running profile. Its constructor takes noILaunchRegistrydependency.SaveAsync(GameProfileSettingsViewModel.Commands.cs:296-453) calls_gameProfileManager.UpdateProfileAsyncwith no process checks and no call toProfileContentLinkerService.UpdateProfileUserDataAsync.ProfileContentLinkerService.UpdateProfileUserDataAsyncis dead code. The method exists with correct delta logic but is never called. Maps only get deployed at launch time viaSwitchProfileUserDataAsyncinGameLauncher.ContentReconciliationServicecan destroy running workspaces.InvalidateWorkspacesForManifestInternalAsync(ContentReconciliationService.cs:386-422) iterates profiles and callsworkspaceManager.CleanupWorkspaceAsync+ clearsActiveWorkspaceIdwithout checkingILaunchRegistry. A background content update can delete a workspace underneath a running game.IGameProfileis a pure data model. Runtime state (IsRunning) lives only inLaunchRegistry(viaGameLaunchInfo.IsRunning) and the view model layer (GameProfileItemViewModel.IsProcessRunning). There is no service-level guard.Requirements
Hotswap eligibility classification
Content types from
ContentType.csmust be classified into two operational classes during active game sessions. This classification should follow the same constant pattern asContentTypePriority(GenHub.Core.Models.Workspace):Hotswappable (deployed to user Documents via
IUserDataTracker, read dynamically by the engine):Map,MapPack— deployed toContentInstallTarget.UserMapsDirectoryNot yet hotswappable (deployed to workspace, no Documents-based loading path exists yet):
Patch,Skin,LanguagePack,Mission— these useContentInstallTarget.Workspaceand 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,UnknownContentTypeImmutable profile metadata (structural properties that define workspace root or launch target):
GameInstallationId,GameClient,WorkspaceStrategy,CustomExecutablePath,WorkingDirectoryFunctional requirements
ProfileContentLinkerService.UpdateProfileUserDataAsyncwithout clearingActiveWorkspaceIdand without disrupting the running game process.IUserDataTracker.UninstallUserDataAsyncmust restore any backed-up files that were overwritten during installation.ContentReconciliationService) must not invalidate or delete workspaces for profiles that are currently running.Proposed Approach
1. Add
ContentHotswapClassificationconstantsCreate a static class alongside
ContentTypePriorityinGenHub.Core.Models.Workspace(orGenHub.Core.Constants) that provides:IsHotswappable(ContentType)— returnstrueforMapandMapPackonly (initial scope)IsLocked(ContentType)— returnstrueforGameInstallation,GameClient,Mod,Addon,Executable,ModdingTool2. Inject
ILaunchRegistryintoGameProfileManagerGive
GameProfileManagerawareness of running state by addingILaunchRegistryas a constructor dependency. InUpdateProfileAsync:ILaunchRegistry.GetAllActiveLaunchesAsync()to determine if the target profile is running.IContentManifestPool.ContentHotswapClassification.IsHotswappable.ActiveWorkspaceId = string.Emptyline atGameProfileManager.cs:239. Persist the profile update normally.ProfileOperationResult.CreateFailurewith a descriptive message and do not mutate profile state.ActiveWorkspaceId, rebuild on next launch).3. Wire
UpdateProfileUserDataAsyncinto the save pathThis is the core of the feature. When
GameProfileSettingsViewModel.SaveAsyncsaves a running profile with hotswappable content changes:_gameProfileManager.UpdateProfileAsyncsucceeds, resolve the full manifests for the newEnabledContentIds.ProfileContentLinkerService.UpdateProfileUserDataAsync(profileId, newManifests, targetGame).Documents\...\Maps\and uninstalls removed maps with backup restoration.The
GameProfileSettingsViewModelwill needIProfileContentLinkerandILaunchRegistry(or a derived boolean) as new dependencies.4. Guard
ContentReconciliationServiceagainst running profilesIn
InvalidateWorkspacesForManifestInternalAsyncandReconcileBulkManifestReplacementInternalAsync:ILaunchRegistrybefore callingworkspaceManager.CleanupWorkspaceAsync.ActiveWorkspaceIdmismatch detection on next launch will handle deferred reconciliation.5. Propagate running state to the editor UI
ILaunchRegistry(or a derivedbool IsProfileRunning) as a dependency toGameProfileSettingsViewModel.GameClient,GameInstallation,Mod,Addon) and workspace-targeted types (Patch,Skin,LanguagePack) and immutable metadata fields (WorkspaceStrategy,GameInstallationdropdown) with explanatory tooltips.MapandMapPack.GameProfileItemViewModel.CanEdit:Acceptance Criteria
ContentHotswapClassification(or equivalent constant class) exists with anIsHotswappable(ContentType)method that returnstrueforMapandMapPack.GameProfileManagerdepends onILaunchRegistryand checks profile running state inUpdateProfileAsyncbefore deciding on workspace invalidation.GameProfileItemViewModel.CanEditpermits opening the profile editor while a profile is running.GameProfileSettingsViewModeldynamically disables locked fields and workspace-targeted content types, and displays a "Hotswap Mode" indicator when the profile is running. OnlyMap/MapPackcheckboxes remain enabled.MaporMapPackcallsProfileContentLinkerService.UpdateProfileUserDataAsync, which installs the map files as hard links from CAS toDocuments\Command and Conquer Generals Zero Hour Data\Maps\.MaporMapPackcallsIUserDataTracker.UninstallUserDataAsync, which removes the hard links and restores any backed-up files.ContentReconciliationServicedoes not callCleanupWorkspaceAsyncor clearActiveWorkspaceIdon profiles that are currently running.ContentTypevalues, running-profile update validation (acceptMap/MapPack, reject locked types), live user data deployment viaUpdateProfileUserDataAsync, map removal with backup restoration, andContentReconciliationServicerunning-profile guard.Explicitly Out of Scope
GameClientexecutables,GameInstallationarchives,ModBIG archives, orAddonDLLs while the game is running.Patch,Skin, orLanguagePackcontent — these deploy to the workspace, not the user's Documents folder, and no Documents-based loading path exists for them yet.