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
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
YYYYMMDDformat (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:
- Semantic version (when explicitly provided by the publisher or detected in release tags like "v2.3")
- Release date (default for ModDB, CNCLabs, and AODMaps)
- Upload date (when release date is unavailable)
- 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.
API Reference
ManifestIdGenerator
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
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
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)
// 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
// 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
// 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:
- Accepts flexible input: Both integers and version strings
- Removes dots: Strips all dot characters from version strings
- Validates numeric format: Ensures the result is a valid number
- Returns normalized string: Used in the manifest ID's
userVersionsegment
Examples
| Input Version | Normalized Output | Resulting 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
// 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:
// 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 formatContentState 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):
// 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 UpdateAvailableID Comparison Logic
The manifest ID comparison for update detection follows this logic:
Extract base ID: Remove the version component to get the content signature
- From
1.20250110.moddbwestwood.addon.supercolorsnewcolors - Base:
moddbwestwood.addon.supercolorsnewcolors
- From
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
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
// 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 versionPublisher-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)
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:
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
dotnet test --filter "ManifestId"Cross-Platform Determinism
Ensures identical ID generation across all platforms through normalization and consistent processing.
