Skip to content

The Workspace System is the "Virtual File System" of GeneralsHub. It assembles a playable game folder on demand by combining the base game files with enabled mods, maps, and patches, all without modifying the original installation.

Architecture

The system uses a Strategy Pattern to create workspaces, allowing for different trade-offs between isolation, speed, and disk usage.

Reconciliation System

Before creating a workspace, the WorkspaceReconciler analyzes the existing folder to determine the minimal set of operations needed. This enables Incremental Updates (delta patching) rather than full rebuilds.

Delta Logic

The reconciler compares the TargetConfiguration (what files should exist) against the CurrentState (what files currently exist).

  1. Conflict Resolution: If multiple manifests provide the same file (e.g., a mod overwrites INIZH.big), the winner is chosen based on Priority:
    • Mod > Patch > Addon > GameInstallation.
  2. Delta Operations:
    • Add: File is missing.
    • Update: File exists but is outdated (Size mismatch, Hash mismatch, or Broken Symlink).
    • Remove: File exists but is not in the new configuration.
    • Skip: File is already up to date.

TIP

Performance Optimization: The reconciler primarily uses File Size and Modification Time to detect changes. Deep SHA256 hashing is skipped during routine launches to ensure the game starts almost instantly.

Workspace Strategies

The system supports multiple assembly strategies. The HybridCopySymlink strategy is the default and recommended choice.

Balances compatibility with disk usage.

  • Rule:
    • Essential Files (Executables, DLLs, INIs, Scripts, files < 1MB): Copied.
    • Asset Files (.big archives, Audio, Maps): Symlinked.
  • Admin Rights: Required on Windows for Symlinks.
  • Fallback: If Admin rights are missing, it attempts to use Hard Links. If that fails (cross-volume), it falls back to Full Copy.

2. Full Copy

Maximum compatibility, maximum disk usage.

  • Mechanism: Physically copies every file.
  • Pros: 100% isolation; Modifying the workspace never affects the source.
  • Cons: Slowest creation time; High disk usage (2GB+ per profile).

High speed, low disk usage, no Admin rights required.

  • Mechanism: Creates NTFS Hard Links.
  • Constraints: Source and Workspace must be on the same drive volume (e.g., both on C:).
  • Risk: Modifying the file content in the workspace changes the source file because they point to the same data on disk.

Minimum disk usage.

  • Mechanism: Symlinks everything.
  • Pros: Instant creation.
  • Cons: Some game engines (like SAGE) behave unexpectedly when essential config files are symlinked.

CAS Integration

Workspaces are fully integrated with Content Addressable Storage (CAS).

  • Manifests can reference files by Hash (SHA256).
  • Strategies can pull files directly from the CAS pool (.gemini/antigravity/cas/).
  • This allows multiple mods to share common assets without duplication.

File Classification Logic

The Hybrid strategy classifies files as Essential or Non-Essential to determine whether to copy or symlink them.

Classification Algorithm

The IsEssentialFile() method evaluates files based on multiple criteria:

csharp
protected static bool IsEssentialFile(string relativePath, long fileSize)
{
    // 1. Size-based classification
    if (fileSize < 1MB) return true;  // Small files are always copied

    // 2. Extension-based classification
    if (extension in [.exe, .dll, .ini, .cfg, .dat, .xml, .json, .txt, .log])
        return true;

    // 3. C&C-specific essential files
    if (extension in [.big, .str, .csf, .w3d])
        return true;

    // 4. Directory-based classification
    if (directory contains ["mods", "patch", "config", "data", "maps", "scripts"])
        return true;

    // 5. Filename pattern matching
    if (filename contains ["mod", "patch", "config", "generals", "zerohour", "settings"])
        return true;

    // 6. Known non-essential media files
    if (extension in [.tga, .dds, .bmp, .jpg, .png, .wav, .mp3, .ogg, .avi, .mp4, .bik])
        return false;

    // 7. Default to essential for unknown files
    return true;
}

File Size Thresholds

ThresholdPurposeBehavior
< 1 MBSmall filesAlways copied (configs, scripts, executables)
≥ 1 MBLarge filesClassification by extension/directory
≥ 5 MBHash verificationSkipped during routine reconciliation for performance

File Type Detection

Detection is performed using:

  1. File Extension: Primary classification method (case-insensitive)
  2. Directory Path: Files in essential directories are always copied
  3. Filename Patterns: Pattern matching for game-specific files
  4. File Size: Overrides other rules for very small files

Fallback Behavior Chain

The Hybrid strategy implements a three-tier fallback system when creating links for non-essential files:

Fallback Sequence

Implementation Details

csharp
try {
    await CreateSymlinkAsync(destination, source, allowFallback: false);
    symlinkedFiles++;
}
catch (UnauthorizedAccessException) when (AreSameVolume(source, destination)) {
    // Fallback 1: Hard Link (same volume only)
    try {
        await CreateHardLinkAsync(destination, source);
        symlinkedFiles++;  // Still counted as symlinked
    }
    catch (Exception) {
        // Fallback 2: Full Copy
        await CopyFileAsync(source, destination);
        copiedFiles++;
    }
}

Trigger Conditions

FallbackTriggerRequirementsNotes
Symlink → Hard LinkUnauthorizedAccessExceptionSame volumeNo admin rights on Windows
Hard Link → CopyAny exceptionNoneCross-volume or filesystem limitation
Direct CopySymlink fails + different volumesNoneMaximum compatibility

Cross-Volume Detection

csharp
public static bool AreSameVolume(string path1, string path2)
{
    var root1 = Path.GetPathRoot(Path.GetFullPath(path1));
    var root2 = Path.GetPathRoot(Path.GetFullPath(path2));
    return string.Equals(root1, root2, StringComparison.OrdinalIgnoreCase);
}

Permission Checking

  • Windows: Symlinks require SeCreateSymbolicLinkPrivilege (admin rights)
  • Linux/macOS: Symlinks work without special permissions
  • Detection: Attempted at runtime via exception handling (no pre-check)

Workspace Directory Structure

Root Location

Workspaces are created in: .gemini/workspaces/

Full path resolution:

{ApplicationDataPath}/.gemini/workspaces/{WorkspaceId}/

Directory Organization

.gemini/
├── workspaces/
│   ├── {profile-id-1}/              # Workspace for Profile 1
│   │   ├── generals.exe             # Copied essential files
│   │   ├── game.dat                 # Copied config
│   │   ├── Data/                    # Symlinked directory
│   │   │   └── INI/ -> {source}     # Symlink to source
│   │   └── Maps/ -> {source}        # Symlink to large assets
│   └── {profile-id-2}/              # Workspace for Profile 2
│       └── ...
├── antigravity/
│   └── cas/                         # Content Addressable Storage
│       └── {hash}.blob              # Shared content files
└── workspaces.json                  # Metadata file

Metadata Storage

Location: {ApplicationDataPath}/workspaces.json

Structure:

json
[
  {
    "Id": "profile-generals-vanilla",
    "WorkspacePath": "C:/Users/.../workspaces/profile-generals-vanilla",
    "GameClientId": "generals-1.8",
    "Strategy": "HybridCopySymlink",
    "CreatedAt": "2025-03-15T10:30:00Z",
    "LastAccessedAt": "2025-03-15T12:45:00Z",
    "FileCount": 623,
    "TotalSizeBytes": 45678912,
    "ManifestIds": ["generals-base", "mod-shockwave"],
    "ManifestVersions": {
      "generals-base": "1.8",
      "mod-shockwave": "1.2.3"
    },
    "IsPrepared": true,
    "IsValid": true
  }
]

Cleanup Policies

  1. Automatic Cleanup:

    • Workspaces with missing directories are removed from metadata on next scan
    • Orphaned files are detected during reconciliation
  2. Manual Cleanup:

    • CleanupWorkspaceAsync(workspaceId) removes workspace and untracks CAS references
    • Critical: CAS references must be untracked before directory deletion
  3. Workspace Reuse:

    • Existing workspaces are reused if manifest IDs and versions match
    • Strategy changes force recreation
    • ForceRecreate flag bypasses reuse logic

Delta Operations Details

csharp
var fileInfo = new FileInfo(filePath);
if (fileInfo.LinkTarget != null) {
    var targetPath = ResolveAbsolutePath(fileInfo.LinkTarget, filePath);
    if (!File.Exists(targetPath)) {
        // Broken symlink detected
        return true;  // Needs update
    }
}

Detection Method:

  • Check FileInfo.LinkTarget property
  • Resolve relative symlink targets to absolute paths
  • Verify target file existence
  • Broken symlinks trigger WorkspaceDeltaOperation.Update

Hash Mismatch Checks

The reconciler uses a performance-optimized hash verification strategy:

csharp
// Regular files
if (manifestFile.Size > 0 && fileInfo.Length != manifestFile.Size) {
    return true;  // Size mismatch = needs update
}

// Hash verification conditions
if (!string.IsNullOrEmpty(manifestFile.Hash) &&
    (forceFullVerification || fileInfo.Length < 5MB)) {
    var hashMatches = await VerifyFileHashAsync(filePath, manifestFile.Hash);
    if (!hashMatches) {
        return true;  // Hash mismatch = needs update
    }
}

When Deep SHA256 Hashing is Performed

ScenarioHash VerificationReason
Routine LaunchSkipped for files > 5MBPerformance optimization
Small Files (< 5MB)Always performedFast enough to verify
Force VerificationAll filesUser-requested deep scan
Symlink TargetsOnly if forceFullVerificationTrust size match by default
New FilesNever (no existing file)Will be added regardless

Performance Optimization Strategies

  1. Size-First Comparison: File size mismatch is checked before hash computation
  2. Symlink Trust: Valid symlinks with size-matching targets are trusted
  3. Selective Hashing: Only small files (< 5MB) are hashed during routine launches
  4. Skip Operations: Files that are already current generate Skip deltas (no I/O)

Delta Operation Types

csharp
public enum WorkspaceDeltaOperation
{
    Add,     // File missing from workspace
    Update,  // File exists but outdated (size/hash mismatch or broken symlink)
    Remove,  // File exists but not in new manifests
    Skip     // File is already current
}

WorkspaceReconciler Implementation

Scanning Algorithm

csharp
public async Task<List<WorkspaceDelta>> AnalyzeWorkspaceDeltaAsync(
    WorkspaceInfo? workspaceInfo,
    WorkspaceConfiguration configuration,
    bool forceFullVerification = false)
{
    // 1. Build file occurrence map (handles conflicts)
    var fileOccurrences = new Dictionary<string, List<(ManifestFile, ContentType, ManifestId)>>();

    // 2. Resolve conflicts using priority system
    var expectedFiles = ResolveConflicts(fileOccurrences);

    // 3. Scan existing workspace files
    var existingFiles = ScanWorkspaceDirectory(workspacePath);

    // 4. Generate delta operations
    foreach (var (relativePath, manifestFile) in expectedFiles) {
        if (!existingFiles.Contains(relativePath)) {
            deltas.Add(new WorkspaceDelta { Operation = Add, ... });
        } else {
            var needsUpdate = await FileNeedsUpdateAsync(fullPath, manifestFile, forceFullVerification);
            deltas.Add(new WorkspaceDelta {
                Operation = needsUpdate ? Update : Skip,
                ...
            });
        }
    }

    // 5. Identify files to remove
    foreach (var relativePath in existingFiles) {
        if (!expectedFiles.ContainsKey(relativePath)) {
            deltas.Add(new WorkspaceDelta { Operation = Remove, ... });
        }
    }

    return deltas;
}

Data Structures Used

  1. File Occurrence Map:

    csharp
    Dictionary<string, List<(ManifestFile File, ContentType ContentType, string ManifestId)>>
    • Key: Relative file path (case-insensitive)
    • Value: All manifests providing this file
  2. Expected Files Dictionary:

    csharp
    Dictionary<string, ManifestFile>
    • Key: Relative file path (case-insensitive)
    • Value: Winning manifest file after conflict resolution
  3. Existing Files Set:

    csharp
    HashSet<string>
    • Contains all relative paths currently in workspace

Conflict Resolution Priority

csharp
public static class ContentTypePriority
{
    public static int GetPriority(ContentType type) => type switch
    {
        ContentType.Mod => 100,              // Highest priority
        ContentType.Patch => 90,
        ContentType.Addon => 80,
        ContentType.GameInstallation => 10,  // Lowest priority
        _ => 50
    };
}

When multiple manifests provide the same file:

  1. Sort by ContentTypePriority (descending)
  2. Winner's file is used in workspace
  3. Losers are logged as warnings

Performance Characteristics

OperationTime ComplexityNotes
File Occurrence MappingO(n)n = total files across all manifests
Conflict ResolutionO(m log m)m = files with conflicts
Directory ScanO(k)k = existing files in workspace
Delta GenerationO(n + k)Linear scan of expected + existing
Hash VerificationO(h)h = small files (< 5MB) only

Memory Usage

  • File Occurrence Map: ~200 bytes per file entry
  • Expected Files: ~150 bytes per unique file
  • Existing Files Set: ~100 bytes per existing file
  • Delta List: ~250 bytes per delta operation

Typical Profile (600 files):

  • Memory: ~150 KB
  • Scan Time: 50-200ms (without hash verification)
  • With Hashing: 500-2000ms (depends on small file count)

Manifest Selection from Profile

Profile → ManifestPool → WorkspaceManager Flow

GameProfile Structure

csharp
public class GameProfile
{
    public string Id { get; set; }
    public string Name { get; set; }
    public GameClient GameClient { get; set; }
    public List<EnabledContent> EnabledContent { get; set; }  // Mods, patches, addons
    public WorkspaceStrategy PreferredStrategy { get; set; }
}

public class EnabledContent
{
    public string ManifestId { get; set; }
    public ContentType ContentType { get; set; }
    public bool IsEnabled { get; set; }
}

WorkspaceConfiguration Construction

csharp
var configuration = new WorkspaceConfiguration
{
    Id = profile.Id,
    GameClient = profile.GameClient,
    Strategy = profile.PreferredStrategy,
    Manifests = manifestPool.ResolveManifests(profile.EnabledContent),
    WorkspaceRootPath = Path.Combine(appDataPath, ".gemini", "workspaces"),
    BaseInstallationPath = profile.GameClient.InstallationPath,
    ManifestSourcePaths = BuildManifestSourcePaths(profile.EnabledContent)
};

Manifest Resolution Process

  1. Profile Activation: User selects a GameProfile
  2. Content Resolution: ManifestPool resolves EnabledContent references to actual ContentManifest objects
  3. Configuration Building: WorkspaceConfiguration is constructed with resolved manifests
  4. Workspace Preparation: WorkspaceManager.PrepareWorkspaceAsync() is called
  5. Strategy Execution: Selected strategy assembles the workspace

Workspace Metadata Persistence

After workspace preparation:

csharp
workspaceInfo.ManifestIds = manifests.Select(m => m.Id.Value).ToList();
workspaceInfo.ManifestVersions = manifests.ToDictionary(
    m => m.Id.Value,
    m => m.Version ?? string.Empty
);
await SaveWorkspaceMetadataAsync(workspaceInfo);

This enables:

  • Fast workspace reuse detection
  • Version change detection (triggers recreation)
  • Manifest change detection (triggers reconciliation)

Performance Characteristics

Strategy Comparison Table

StrategyCreation SpeedDisk UsageAdmin RightsSame VolumeCompatibilityUse Case
Hybrid Copy-SymlinkMediumLow-MediumYes (Windows)NoHighRecommended default
Full CopySlowHigh (2GB+)NoNoMaximumTesting, isolation
Hard LinkFastVery LowNoYesMediumSame-drive setups
Symlink OnlyInstantMinimalYes (Windows)NoLowAdvanced users

Benchmarks (600-file workspace)

Creation Time

StrategyFirst CreationIncremental UpdateNotes
Hybrid2-5 seconds100-500msCopies ~50MB, symlinks ~1.5GB
Full Copy15-30 seconds15-30 secondsCopies entire 2GB
Hard Link1-2 seconds50-200msSame volume only
Symlink500ms-1s50-100msRequires admin

Disk Usage

StrategyTypical UsageExplanation
Hybrid50-200 MBEssential files copied, assets symlinked
Full Copy2-3 GBComplete duplication
Hard Link< 1 MBMetadata only (same inode)
Symlink< 1 MBLink overhead only

Compatibility Score

StrategyWindowsLinuxmacOSCross-DriveNotes
Hybrid⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐Admin required on Windows
Full Copy⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐Works everywhere
Hard Link⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐Same volume only
Symlink⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐Admin required on Windows

When to Use Each Strategy

  1. Hybrid Copy-Symlink (Default):

    • General use
    • Cross-drive installations
    • Balance between speed and disk usage
  2. Full Copy:

    • Testing mod conflicts
    • Complete isolation needed
    • Disk space is not a concern
  3. Hard Link:

    • Game and workspace on same drive
    • No admin rights available
    • Maximum speed required
  4. Symlink Only:

    • Development/testing
    • Admin rights available
    • Minimal disk usage critical

Troubleshooting

Permission Errors

Symptom: UnauthorizedAccessException when creating symlinks

Cause: Windows requires admin rights for symlink creation

Solutions:

  1. Run GeneralsHub as Administrator
  2. Enable Developer Mode (Windows 10+):
    • Settings → Update & Security → For Developers → Developer Mode
  3. Switch to Hard Link strategy (same volume only)
  4. Switch to Full Copy strategy (slower but no permissions needed)

Detection:

csharp
public override bool RequiresAdminRights =>
    Environment.OSVersion.Platform == PlatformID.Win32NT;

Cross-Drive Failures

Symptom: Hard link creation fails with "The system cannot move the file to a different disk drive"

Cause: Hard links require source and destination on same volume

Solutions:

  1. Switch to Hybrid or Symlink strategy
  2. Move game installation to same drive as workspace
  3. Change workspace root path to same drive as game

Detection:

csharp
if (!AreSameVolume(sourcePath, destinationPath)) {
    throw new IOException("Hard links require same volume");
}

Corrupted Files

Symptom: Game crashes or behaves unexpectedly

Cause: Hash mismatch or incomplete file copy

Solutions:

  1. Force full verification:

    csharp
    var deltas = await reconciler.AnalyzeWorkspaceDeltaAsync(
        workspaceInfo,
        configuration,
        forceFullVerification: true
    );
  2. Force workspace recreation:

    csharp
    configuration.ForceRecreate = true;
    await workspaceManager.PrepareWorkspaceAsync(configuration);
  3. Check source files integrity

  4. Clear CAS cache if using CAS-backed content

Prevention:

  • Hash verification for essential files (< 5MB)
  • Size checks for all files
  • Broken symlink detection

Symptom: Symlinks point to wrong location or are broken

Cause: Source files moved, deleted, or relative path resolution failed

Solutions:

  1. Check symlink target:

    bash
    # Windows
    dir /AL workspace_path
    
    # Linux/macOS
    ls -la workspace_path
  2. Verify source files exist

  3. Force workspace recreation

  4. Switch to Full Copy strategy temporarily

Reconciler Detection:

csharp
if (fileInfo.LinkTarget != null) {
    var targetPath = ResolveAbsolutePath(fileInfo.LinkTarget, filePath);
    if (!File.Exists(targetPath)) {
        // Broken symlink - will be recreated
        return true;
    }
}

Workspace Reuse Failures

Symptom: Workspace recreated every launch despite no changes

Cause: Manifest version mismatch or metadata corruption

Diagnosis:

csharp
// Check workspace metadata
var workspaces = await workspaceManager.GetAllWorkspacesAsync();
var workspace = workspaces.Data.FirstOrDefault(w => w.Id == profileId);

// Compare manifest versions
var currentVersions = profile.EnabledContent.Select(c => c.Version);
var cachedVersions = workspace.ManifestVersions;

Solutions:

  1. Verify manifest versions are stable
  2. Check workspaces.json for corruption
  3. Clear workspace metadata and recreate
  4. Ensure ForceRecreate is not always set

Performance Issues

Symptom: Slow workspace creation or game launch

Causes & Solutions:

  1. Deep hash verification on every launch:

    • Disable forceFullVerification for routine launches
    • Only enable for troubleshooting
  2. Full Copy strategy on large installations:

    • Switch to Hybrid or Hard Link strategy
    • Reduces disk I/O significantly
  3. Antivirus scanning:

    • Add workspace directory to antivirus exclusions
    • Exclude .gemini/workspaces/ and .gemini/antigravity/cas/
  4. Slow disk (HDD):

    • Move workspace root to SSD
    • Use Hard Link strategy (same volume)
    • Reduce number of enabled mods

Monitoring:

csharp
var stopwatch = Stopwatch.StartNew();
await workspaceManager.PrepareWorkspaceAsync(configuration, progress);
logger.LogInformation("Workspace prepared in {Elapsed}ms", stopwatch.ElapsedMilliseconds);

GeneralsHub Docs