Skip to content

Manifest ID System ​

NOTE

This document details the Manifest ID System and prefix-matching state detection logic utilized by the Unified Downloads Browser introduced in PR #265 (feat/ui-downloads).

Overview ​

The Manifest ID system provides deterministic, human-readable, and type-safe identifiers for all content in the GenHub ecosystem. This system ensures consistent content identification across platforms, prevents ID collisions, and provides robust validation with proper error handling.

Architecture ​

The system follows a layered architecture with clear separation of concerns:

Core Layer: ManifestIdGenerator ​

Low-level utility for generating deterministic, cross-platform manifest IDs with advanced normalization and filesystem-safe output.

Service Layer: ManifestIdService ​

Implements the ResultBase pattern for type-safe operations, wrapping the generator with proper error handling and returning ContentOperationResult<ManifestId>.

Validation Layer: ManifestIdValidator ​

Comprehensive validation ensuring ID format compliance and security through regex-based rules and format verification.

Type Safety Layer: ManifestId ​

Strongly-typed value object with compile-time validation, implicit conversions, and JSON serialization support.

Integration Layer ​

Seamless integration into ContentManifestBuilder, ManifestGenerationService, and other components.

ID Formats ​

Unified 5-Segment Format ​

All content in GenHub uses a consistent 5-segment format:

Format: schemaVersion.userVersion.publisher.contentType.contentName

This unified approach treats platform distributors (Steam, EA, etc.) as publishers, just like community content creators.

Game Installations and Clients ​

Examples:

  • Steam ZeroHour v1.04 Installation: 1.104.steam.gameinstallation.zerohour
  • EA App Generals v1.08 Client: 1.108.eaapp.gameclient.generals
  • Retail Generals v1.08: 1.108.retail.gameinstallation.generals
  • Custom Version 0: 1.0.steam.gameinstallation.generals

Publisher Attribution: Platform name (Steam, EA, Retail, etc.) is treated as the publisher.

Version Format: The userVersion segment accepts either integers (0, 1, 2) or version strings ("1.08", "1.04"). Version strings automatically have dots removed for schema compliance:

  • "1.08" → "108" (Generals executable version)
  • "1.04" → "104" (Zero Hour executable version)
  • 0 → "0" (default/first version)

This normalization ensures the manifest ID schema remains valid (dots separate segments) while supporting human-readable version strings.

Community Content ​

Examples:

  • GenHub Mod: 1.0.genhub.mod.custom-mod
  • GeneralsOnline Client: 1.0.generalsonline.gameclient.generalsonline_30hz
  • CNC Labs Map: 1.0.cnclabs.map.desert-storm
  • WorldBuilder Tool: 1.0.ea.moddingtool.worldbuilder
  • GenLauncher Zero Hour Mod: 1.0.genlauncherzerohour.mod.riseofthereds187publicbuild20
  • GenLauncher Generals Mod: 1.0.genlaunchergenerals.mod.generalsimproved

Publisher Attribution: Community publisher name (e.g., "genhub", "generalsonline", "cnclabs")

ModDB Format ​

Format: {schemaVersion}.{dateVersion}.moddb-{author}.{contentType}.{contentName}

Components:

  • schemaVersion: Always 1
  • dateVersion: Release date in YYYYMMDD format (extracted from ModDB's "Added" field)
  • publisher: Uses moddb-{author} as the publisher segment (e.g., moddb-westwood)
  • contentType: Content type (mod, addon, map, etc.)
  • contentName: Normalized content name

Examples:

  • ModDB Addon (release date 2025-01-20, author westwood): 1.20250120.moddbwestwood.addon.supercolorsnewcolors
  • ModDB Mod (release date 2024-12-15, author contra-team): 1.20241215.moddbcontrateam.mod.contra007
  • ModDB Map Pack (release date 2025-01-10, author mappackers): 1.20250110.moddbmappackers.mappack.desertstormcollection

Key Points:

  • Date-based versioning: Uses the release date (YYYYMMDD format) as the version component
  • No semantic version parsing: ModDB content titles are not parsed for semantic versions (v1.0, etc.)
  • Deterministic: Same content + same release date = same manifest ID
  • Publisher identifier: Uses moddb-{author} as the publisher segment
  • Date source: Extracted from the ModDB page's release date metadata during content discovery

Version Fallback Priority:

When generating manifest IDs for content, the version is determined by the following priority order:

  1. Semantic version (when explicitly provided by the publisher or detected in release tags like "v2.3")
  2. Release date (default for ModDB, CNCLabs, and AODMaps)
  3. Upload date (when release date is unavailable)
  4. Discovery date (fallback when no other date information is available)

This fallback hierarchy ensures that content always has a valid version component for the manifest ID, with preference given to publisher-provided semantic versions when available.

GenLauncher Format ​

Format: {schemaVersion}.{userVersion}.{publisher}{gameToken}.{contentType}.{slug}

Components:

  • schemaVersion: Always 1
  • userVersion: 0
  • publisher: Base publisher identifier (genlauncher)
  • gameToken: Normalized game suffix (generals or zerohour), producing genlaunchergenerals or genlauncherzerohour
  • contentType: Content type (mod, patch, addon, etc.)
  • slug: Alphanumeric normalized slug generated from mod title and release name

Examples:

  • GenLauncher Zero Hour Mod: 1.0.genlauncherzerohour.mod.riseofthereds187publicbuild20
  • GenLauncher Generals Mod: 1.0.genlaunchergenerals.mod.generalsimproved

Key Points:

  • Game-differentiated publisher ID: Concatenates game token to isolate Zero Hour and Generals content into distinct publisher namespaces
  • Multi-release synthetic rows: Sibling releases and files use file:... synthetic rows disambiguated by URL, version, and slug matching
  • Cold-restart recovery: Reconstructed reliably by ContentStateService.DetermineProspectiveManifestId without requiring an active session cache

API Reference ​

ManifestIdGenerator ​

csharp
public static class ManifestIdGenerator
{
    // Generate publisher content ID with content type (used for all content)
    public static string GeneratePublisherContentId(
        string publisherId,
        ContentType contentType,
        string contentName,
        int userVersion = 0,
        string suffix = "");

    // Generate game installation ID (uses 5-segment publisher format)
    // Note: Installation type (Steam, EA, etc.) is treated as the publisher
    // Note: userVersion accepts both integers (0, 1, 2) and version strings ("1.08", "1.04")
    // Version strings have dots automatically removed for schema compliance: "1.08" → "108"
    public static string GenerateGameInstallationId(
        GameInstallation installation,
        GameType gameType,
        object? userVersion);
}

ManifestIdService ​

csharp
public class ManifestIdService : IManifestIdService
{
    // Generate publisher content ID with OperationResult pattern
    OperationResult<ManifestId> GeneratePublisherContentId(
        string publisherId,
        ContentType contentType,
        string contentName,
        int userVersion = 0,
        string suffix = "");

    // Generate game installation ID with OperationResult pattern (uses 5-segment format)
    // Note: Installation type (Steam, EA, etc.) is treated as the publisher
    // Note: userVersion accepts both integers (0, 1, 2) and version strings ("1.08", "1.04")
    // Version strings have dots automatically removed for schema compliance: "1.08" → "108"
    OperationResult<ManifestId> GenerateGameInstallationId(
        GameInstallation installation,
        GameType gameType,
        object? userVersion);

    // Validate and create ManifestId
    OperationResult<ManifestId> ValidateAndCreateManifestId(string manifestIdString);
}

ManifestId Struct ​

csharp
public readonly struct ManifestId : IEquatable<ManifestId>
{
    public string Value { get; }

    // Implicit conversions
    public static implicit operator ManifestId(string id);
    public static implicit operator string(ManifestId id);

    // Validation and creation
    public static ManifestId Create(string id);

    // Equality operations
    public static bool operator ==(ManifestId left, ManifestId right);
    public static bool operator !=(ManifestId left, ManifestId right);

    // Object methods
    public override bool Equals(object? obj);
    public override int GetHashCode();
    public override string ToString();
}

Usage Examples ​

Game Installation IDs (5-Segment Format) ​

csharp
// Generate ID for detected game installation with integer version
var installation = new GameInstallation("C:\\Games\\Generals", GameInstallationType.Steam);
var gameType = GameType.Generals;

// Using service with integer version (default 0)
var idResult = _manifestIdService.GenerateGameInstallationId(installation, gameType, 0);
if (idResult.Success)
{
    ManifestId id = idResult.Data; // 1.0.steam.gameinstallation.generals
}

// Using string version for Generals 1.08
var idResult = _manifestIdService.GenerateGameInstallationId(installation, gameType, "1.08");
if (idResult.Success)
{
    ManifestId id = idResult.Data; // 1.108.steam.gameinstallation.generals
}

// Using string version for Zero Hour 1.04
var zhInstallation = new GameInstallation("C:\\Games\\ZeroHour", GameInstallationType.Steam);
var idResult = _manifestIdService.GenerateGameInstallationId(zhInstallation, GameType.ZeroHour, "1.04");
if (idResult.Success)
{
    ManifestId id = idResult.Data; // 1.104.steam.gameinstallation.zerohour
}

// Using generator directly with version constant
string idString = ManifestIdGenerator.GenerateGameInstallationId(
    installation,
    gameType,
    ManifestConstants.GeneralsManifestVersion); // "1.08" → generates "1.108.steam.gameinstallation.generals"

Publisher Content IDs ​

csharp
// Generate ID for content with publisher and content type
var idResult = _manifestIdService.GeneratePublisherContentId("genhub", ContentType.Mod, "custom-mod", 0);
if (idResult.Success)
{
    ManifestId id = idResult.Data; // 1.0.genhub.mod.custom-mod
}

// Generate ID for GeneralsOnline client
var clientResult = _manifestIdService.GeneratePublisherContentId("generalsonline", ContentType.GameClient, "generalsonline_30hz", 0);
if (clientResult.Success)
{
    ManifestId id = clientResult.Data; // 1.0.generalsonline.gameclient.generalsonline_30hz
}

// Generate ID for ModDB content with date-based version
var moddbResult = _manifestIdService.GeneratePublisherContentId("moddb-westwood", ContentType.Addon, "supercolors-newcolors", 20250120);
if (moddbResult.Success)
{
    ManifestId id = moddbResult.Data; // 1.20250120.moddbwestwood.addon.supercolorsnewcolors
}

// Generate ID for ModDB modpack with date version
var moddbModResult = _manifestIdService.GeneratePublisherContentId("moddb-contra-team", ContentType.Mod, "contra-007", 20241215);
if (moddbModResult.Success)
{
    ManifestId id = moddbModResult.Data; // 1.20241215.moddbcontrateam.mod.contra007
}

Validation ​

csharp
// Validate existing ID (5-segment format required)
var validation = _manifestIdService.ValidateAndCreateManifestId("1.0.steam.gameinstallation.generals");
if (validation.Success)
{
    ManifestId id = validation.Data;
}

Version Normalization ​

The manifest ID system automatically normalizes version values to ensure schema compliance. The manifest ID format uses dots (.) to separate segments, so version strings containing dots are normalized by removing them.

How It Works ​

The NormalizeVersionString() method processes version values as follows:

  1. Accepts flexible input: Both integers and version strings
  2. Removes dots: Strips all dot characters from version strings
  3. Validates numeric format: Ensures the result is a valid number
  4. Returns normalized string: Used in the manifest ID's userVersion segment

Examples ​

Input VersionNormalized OutputResulting Manifest ID
0"0"1.0.steam.gameinstallation.generals
1"1"1.1.steam.gameinstallation.generals
"1.08""108"1.108.steam.gameinstallation.generals
"1.04""104"1.104.steam.gameinstallation.zerohour
"2.0""20"1.20.steam.gameinstallation.generals

Why Version Normalization? ​

Schema Compliance: The manifest ID format uses dots (.) to separate segments. Each segment must not contain additional dots. If the userVersion segment contained dots (e.g., "1.08"), it would break the 5-segment schema:

  • ❌ Invalid: 1.1.08.steam.gameinstallation.generals (6 segments instead of 5)
  • ✅ Valid: 1.108.steam.gameinstallation.generals (5 segments as expected)

Human-Readable Versions: Developers can use familiar version strings like "1.08" or "1.04" that match the actual game executable versions, while the system automatically handles the normalization for schema compliance.

Backward Compatibility: Existing code using integer versions (0, 1, 2) continues to work without changes, as integers are converted to strings without modification.

Usage ​

csharp
// Using manifest version constants (automatically normalized)
using static GenHub.Core.Constants.ManifestConstants;

var generalsId = ManifestIdGenerator.GenerateGameInstallationId(
    installation,
    GameType.Generals,
    GeneralsManifestVersion); // "1.08" → "108"
// Result: "1.108.steam.gameinstallation.generals"

var zhId = ManifestIdGenerator.GenerateGameInstallationId(
    zhInstallation,
    GameType.ZeroHour,
    ZeroHourManifestVersion); // "1.04" → "104"
// Result: "1.104.steam.gameinstallation.zerohour"

// Using custom version strings
var customId = ManifestIdGenerator.GenerateGameInstallationId(
    installation,
    GameType.Generals,
    "2.0"); // "2.0" → "20"
// Result: "1.20.steam.gameinstallation.generals"

// Using integer versions (no normalization needed)
var defaultId = ManifestIdGenerator.GenerateGameInstallationId(
    installation,
    GameType.Generals,
    0); // 0 → "0"
// Result: "1.0.steam.gameinstallation.generals"

Version Normalization Validation ​

The normalization method validates that the result is numeric after dot removal:

csharp
// Valid inputs
NormalizeVersionString("1.08");  // ✅ Returns "108"
NormalizeVersionString("1.04");  // ✅ Returns "104"
NormalizeVersionString(5);       // ✅ Returns "5"
NormalizeVersionString("2.0");   // ✅ Returns "20"

// Invalid inputs (throws ArgumentException)
NormalizeVersionString("1.0a");  // ❌ Contains letters
NormalizeVersionString("v1.08"); // ❌ Contains letters
NormalizeVersionString("1..08"); // ❌ Results in "108" but has invalid format

ContentState Integration ​

The Manifest ID system is deeply integrated with the ContentState tracking system to detect content updates and manage installation states.

State Detection through Manifest IDs ​

ContentState uses manifest IDs as the primary key for tracking content across different publishers. The system supports:

  • Downloaded state (ContentState.Downloaded): Content that has been acquired and stored in the manifest pool
  • UpdateAvailable state (ContentState.UpdateAvailable): A newer version of existing content is available
  • NotDownloaded state (ContentState.NotDownloaded): Content that is discovered or available in a publisher catalog but not currently downloaded

Prefix Matching for Update Detection ​

The system uses prefix matching to detect updates for content with date-based versioning (like ModDB):

csharp
// Example: Detecting updates for ModDB content
// Downloaded (current): 1.20250110.moddbwestwood.addon.supercolorsnewcolors
// Discovered (newer):   1.20250120.moddbwestwood.addon.supercolorsnewcolors

// The system compares:
// - Schema version (1) - must match
// - Publisher (moddbwestwood) - must match
// - Content type (addon) - must match
// - Content name (supercolorsnewcolors) - must match
// - Version (20250110 vs 20250120) - used to determine if newer

// Since the base ID (excluding version) matches and the available version
// is newer (higher date), the state is set to UpdateAvailable

ID Comparison Logic ​

The manifest ID comparison for update detection follows this logic:

  1. Extract base ID: Remove the version component to get the content signature

    • From 1.20250110.moddbwestwood.addon.supercolorsnewcolors
    • Base: moddbwestwood.addon.supercolorsnewcolors
  2. Compare signatures: Check if installed and available content have the same base

    • If base IDs match → same content, compare versions
    • If base IDs differ → different content entirely
  3. Version comparison: For matching base IDs, determine if update available

    • Date-based versions (YYYYMMDD): Higher numeric value = newer version
    • Semantic versions (normalized): Standard semantic version comparison
    • Integer versions: Higher integer value = newer version

Practical Example ​

csharp
// Scenario: ModDB content update detection

// 1. User downloads "Super Colors" addon on January 10, 2025
// Manifest is stored in IContentManifestPool:
// Manifest ID: "1.20250110.moddbwestwood.addon.supercolorsnewcolors"

// 2. System discovers updated version released on January 20, 2025
// Discovered ContentSearchResult generates prospective ID:
// "1.20250120.moddbwestwood.addon.supercolorsnewcolors"

// 3. ContentStateService (GenHub.Features.Downloads.Services, introduced in PR #265) detects update:
//    - Inspects IContentManifestPool for matching base signature: "moddbwestwood.addon.supercolorsnewcolors"
//    - Compares date versions: 20250120 > 20250110
//    - Returns: ContentState.UpdateAvailable

// 4. UI shows "Update" button on the content card (DownloadsBrowserViewModel)
//    User can click to download and install the newer version

Publisher-Specific Behavior ​

Different publishers interact with the ContentState system differently:

ModDB (Date-based versioning):

  • Each new release date creates a new manifest ID
  • Updates detected when same content has newer release date
  • Historical versions tracked separately (different IDs)

GitHub (Semantic versioning):

  • Explicit version tags (v1.0, v2.0) used in manifest ID
  • Updates follow semantic version rules
  • Pre-release handling supported (beta, alpha tags)

GenLauncher (Game-specific provider with multi-release packages):

  • Prospective IDs incorporate game type tokens (genlauncherzerohour / genlaunchergenerals).
  • Multi-release mods contain synthetic child file rows (file:...).
  • ContentStateService matches persisted manifests by checking source URL, normalized version strings, titles, and slug segments (FileRowMatchesManifest), avoiding cross-release false positives.
  • ContentDetailViewModel reconciles matching releases across multi-release lists upon page load or variant selection, propagating IsDownloaded = true and DownloadedManifestId so action buttons accurately transition between "Download Now" and "Add to Profile" after application restarts.

Creator Publishing (User-specified versions):

  • Publisher defines version in catalog JSON
  • Semantic or date-based at publisher discretion
  • System respects publisher's version scheme

Validation Rules ​

All Content (5-Segment Format) ​

  • Format: schemaVersion.userVersion.publisher.contentType.contentName
  • UserVersion: Accepts integers (0, 1, 2) or version strings ("1.08", "1.04"). Version strings have dots removed during normalization ("1.08" → "108")
  • Publisher: Can be platform (steam, eaapp, retail) or community publisher (genhub, generalsonline, cnclabs, moddb)
  • ContentType: Must be valid content type (gameinstallation, gameclient, mod, patch, addon, mappack, languagepack, moddingtool, etc.)
  • ContentName: Alphanumeric with dashes (e.g., "generals", "custom-mod")
  • Total Segments: Exactly 5 segments required

Error Handling ​

Uses ResultBase pattern for robust error handling:

csharp
var result = _manifestIdService.GeneratePublisherContentId("invalid", ContentType.Mod, "content", 0);
if (!result.Success)
{
    _logger.LogError($"ID generation failed: {result.ErrorMessage}");
}

Integration Points ​

ContentManifestBuilder ​

  • Automatically generates and validates IDs
  • Uses ManifestIdService for consistency

ManifestGenerationService ​

  • Uses ManifestIdService for all manifest creation
  • Ensures deterministic ID generation

ManifestProvider ​

  • Validates manifest IDs during loading
  • Uses IManifestIdService for operations

Testing ​

Comprehensive test coverage in ManifestIdGeneratorTests.cs and related test files.

Running Tests ​

bash
dotnet test --filter "ManifestId"

Cross-Platform Determinism ​

Ensures identical ID generation across all platforms through normalization and consistent processing.

GeneralsHub Docs