Dev Catalog and Download System - FitzDegenhub/UltimateCameraMod GitHub Wiki
This document covers the preset catalog infrastructure, download flow, update detection mechanism, session XML baking for UCM presets, and the .catalog_state.json sidecar format.
UCM distributes presets through two GitHub-hosted catalogs:
| Source | Repository | Branch / Path | Catalog URL |
|---|---|---|---|
| UCM Presets | FitzDegenhub/UltimateCameraMod |
main branch, /ucm_presets/
|
https://raw.githubusercontent.com/FitzDegenhub/UltimateCameraMod/main/ucm_presets/catalog.json |
| Community Presets | FitzDegenhub/UltimateCameraMod |
main branch, /community_presets/
|
https://raw.githubusercontent.com/FitzDegenhub/UltimateCameraMod/main/community_presets/catalog.json |
Both catalogs are fetched over HTTPS from GitHub's raw content CDN. The app uses HttpClient with a 10-second timeout and a User-Agent header of UltimateCameraMod/3.0.
The catalog is a JSON object with a top-level presets array. Each entry in the array has the following shape:
{
"presets": [
{
"id": "panoramic-v2",
"file": "panoramic-v2.ucmpreset",
"name": "Panoramic v2",
"author": "0xFitz",
"description": "Wide cinematic camera with smooth transitions.",
"url": "https://www.nexusmods.com/crimsondesert/mods/438",
"version": "1.2",
"sha256": "a1b2c3d4e5f6...",
"size_bytes": 48320,
"tags": ["cinematic", "wide", "steadycam"]
}
]
}| Field | Type | Purpose |
|---|---|---|
id |
string |
Unique identifier for the preset. Used as fallback filename for community presets ({id}.ucmpreset). |
file |
string |
Filename of the preset file in the repository. UCM presets use this directly as the local filename. |
name |
string |
Human-readable display name shown in the browser card UI. |
author |
string |
Creator attribution displayed below the preset name. |
description |
string |
Truncated to 200 characters in the card UI. Full text stored in the preset file itself. |
url |
string |
Optional Nexus Mods or external link. If present, a "Nexus" button appears on the card. |
version |
string |
Semantic version string. Currently informational only; not used for update comparison. |
sha256 |
string |
Lowercase hex SHA-256 hash of the preset file. The primary mechanism for update detection. |
size_bytes |
long |
File size in bytes. Currently informational; the app enforces a separate 2 MB download cap. |
tags |
string[] |
Tag labels rendered as pill badges on the card. Used for visual categorization, not filtering. |
In CommunityBrowserDialog.xaml.cs, catalog entries are deserialized into a private sealed class:
private sealed class CatalogEntry
{
public string Id { get; set; } = "";
public string File { get; set; } = "";
public string Name { get; set; } = "";
public string Author { get; set; } = "";
public string Description { get; set; } = "";
public string Url { get; set; } = "";
public string Version { get; set; } = "";
public string Sha256 { get; set; } = "";
public long SizeBytes { get; set; }
public string[] Tags { get; set; } = Array.Empty<string>();
}Parsing uses System.Text.Json.JsonDocument with manual property extraction via TryGetProperty. This is deliberate: it makes the parser tolerant of missing fields and avoids exceptions when the catalog evolves.
Presets are stored in directories relative to the executable:
UltimateCameraMod.V3.exe
ucm_presets/
catalog_state.json <-- sidecar for update detection
panoramic-v2.ucmpreset
heroic.ucmpreset
...
my_presets/
my-custom-camera.ucmpreset
community_presets/
some-community-preset.ucmpreset
...
import_presets/
...
The directory constants are defined in MainWindow.xaml.cs:
private const string UcmPresetsDirName = "ucm_presets";
private const string MyPresetsDirName = "my_presets";
private const string CommunityPresetsDirName = "community_presets";
private const string ImportPresetsDirName = "import_presets";The CommunityBrowserDialog is a WPF Window that serves both UCM and community preset browsing. It is parameterized at construction time:
public CommunityBrowserDialog(
string presetsDir,
Action onPresetsChanged,
string catalogUrl = "https://raw.githubusercontent.com/FitzDegenhub/UltimateCameraMod/main/community_presets/catalog.json",
string rawBaseUrl = "https://raw.githubusercontent.com/FitzDegenhub/UltimateCameraMod/main/community_presets/",
string title = "Community Presets",
bool needsSessionXmlBake = false)When opened for UCM presets, the caller passes:
-
catalogUrl: points to themainbranch catalog -
rawBaseUrl: the raw GitHub base URL for that branch -
needsSessionXmlBake: true: signals that downloaded presets contain only style definitions and need session XML generation
When opened for community presets, defaults apply and needsSessionXmlBake is false.
-
Fetch catalog:
FetchCatalogAsync()fires onLoaded. The raw JSON is fetched, parsed, and stored in_catalog. -
Render cards:
RenderPresetList()iterates the catalog entries. For each entry,IsPresetDownloaded()checks whether the file already exists on disk. Downloaded presets show a disabled "Downloaded" button; others show an active "Download" button. -
Download click:
OnDownloadClickhandler:- Disables the button and shows "Downloading..."
- Fetches raw bytes from
_rawBaseUrl + entry.File - Enforces a 2 MB size cap (
MaxPresetSize = 2 * 1024 * 1024) - Validates the downloaded content is valid JSON containing either
session_xml,RawXml, orstyle_id - Saves raw bytes to disk (not re-serialized, to preserve the exact SHA hash)
- Updates
.catalog_state.jsonwith the entry's SHA-256 hash (UCM presets only) - Invokes
_onPresetsChangedcallback to refresh the sidebar
-
Filename resolution:
- UCM presets (
needsSessionXmlBake == true): useentry.Filedirectly as the filename - Community presets with a
Filefield: usePath.GetFileName(entry.File) - Fallback:
{entry.Id}.ucmpreset
- UCM presets (
In addition to the browser dialog, UCM performs an automatic background fetch on every launch via FetchUcmPresetsAsync() in MainWindow.Community.cs. This method:
- Fetches the UCM catalog
- Iterates entries and checks whether each file exists locally
- Downloads only missing presets (does not update existing ones)
- Updates
.catalog_state.jsonfor each new download - Calls
RefreshPresetManagerListson the dispatcher to update the sidebar
Existing presets with SHA mismatches are handled separately by the update detection system (see below), not by this initial fetch.
The .catalog_state.json file is a simple JSON dictionary mapping filenames to their SHA-256 hashes. It lives inside the ucm_presets/ directory.
{
"panoramic-v2.ucmpreset": "a1b2c3d4e5f6789...",
"heroic.ucmpreset": "9f8e7d6c5b4a321..."
}This sidecar file records the SHA-256 hash of each preset file at the time it was downloaded from the catalog. It serves as the "last known remote state" for update detection.
Writing (after download):
string statePath = Path.Combine(_presetsDir, ".catalog_state.json");
var state = new Dictionary<string, string>();
if (File.Exists(statePath))
{
string stateJson = File.ReadAllText(statePath);
state = JsonSerializer.Deserialize<Dictionary<string, string>>(stateJson) ?? new();
}
state[destFileName] = entry.Sha256;
File.WriteAllText(statePath,
JsonSerializer.Serialize(state, new JsonSerializerOptions { WriteIndented = true }));Reading (during update check): ReadCatalogState() deserializes the file into a Dictionary<string, string>.
Downloaded preset files are written to disk as raw bytes (File.WriteAllBytesAsync), not re-serialized from parsed JSON. This is critical because re-serialization would alter formatting, whitespace, and key ordering, which would produce a different SHA-256 hash and break update detection.
UCM checks for preset updates at launch via two async methods:
-
CheckUcmPresetUpdatesAsync()for UCM presets -
CheckCommunityPresetUpdatesAsync()for community presets
The algorithm for UCM presets:
- Fetch the remote catalog and build a
Dictionary<string, string>offilename -> sha256 - Read the local
.catalog_state.jsonsidecar - For each preset in the sidebar:
- Skip non-UCM presets and placeholders
- Look up the filename in the sidecar. If there is no sidecar entry, skip (the preset was manually placed or pre-installed, not downloaded through the app)
- Compare the sidecar's stored SHA against the catalog's SHA
- If they differ, set
item.HasUpdate = trueon thePresetManagerItem
Community presets do not use the sidecar file. Instead, the local file's SHA-256 is computed on the fly:
- Fetch the remote catalog and build a
Dictionary<string, string>ofid -> sha256 - For each community preset in the sidebar:
- Read the local file bytes and compute
SHA256.HashData(bytes) - Compare against the catalog hash
- If they differ, set
HasUpdate = true
- Read the local file bytes and compute
When HasUpdate is true on a PresetManagerItem, the sidebar displays a pulsating update icon next to the preset name. Clicking this icon triggers OnPresetUpdateClick.
OnPresetUpdateClick presents a three-way dialog:
-
Yes: Duplicate the current version to
my_presets/with_oldsuffix before downloading the update - No: Overwrite directly without saving the old version
- Cancel: Abort the update
The duplication logic:
string dupName = $"{item.Name}_old";
string dupPath = Path.Combine(MyPresetsDir, $"{SanitizeFileStem(dupName)}.ucmpreset");
// Read existing file, change name/kind/locked, write to my_presetsAfter downloading the update:
- UCM presets: raw bytes are written and the sidecar is updated
- Community presets: the file is rebuilt with metadata (name, author, description, url) preserved from the existing file where the download lacks those fields
After update, both CheckUcmPresetUpdatesAsync and CheckCommunityPresetUpdatesAsync are re-invoked to refresh update icons on other presets.
UCM presets and community presets differ fundamentally in what they contain:
| Preset Type | Contains session_xml? |
Needs Baking? |
|---|---|---|
| UCM Preset | No (style definition only) | Yes |
| Community Preset | Yes (fully baked) | No |
The session_xml is the complete, modified playercamerapreset.xml content that gets compressed and written into the game's 0.paz archive. It is the final output of the camera modification pipeline.
UCM preset files in the repository contain a style definition (parameters like style_id, distance, height, offset values) but no session_xml. The session XML must be generated locally because it depends on the user's game installation:
-
Read vanilla XML:
CameraMod.ReadVanillaXml(gameDir)extracts the unmodified camera XML from the game's PAZ archive or backup -
Apply CameraRules: The style definition and user settings are passed through
CameraRules.BuildModifications()to produce aModificationSet -
Generate modified XML:
CameraMod.ApplyModifications(vanillaXml, modSet)produces the final session XML -
Store: The session XML is saved into the preset file's
session_xmlfield
This baking is performed by GenerateBuiltInPresets(), which is called:
- After the game directory is detected
- After new UCM presets are downloaded via the browser dialog
Community presets are authored by third parties who have already generated the full session XML against a specific game version. The preset file shipped in the community repository contains the complete session_xml, so it can be used directly without access to the user's vanilla game files. This also means community presets are tied to a specific game version and may need updates when the game patches.
The CommunityBrowserDialog constructor accepts a needsSessionXmlBake boolean. When true:
- The header subtitle changes to mention "official UCM camera presets"
- Downloaded files use
entry.Filedirectly as the local filename (not justPath.GetFileName) - The main window's
onPresetsChangedcallback triggersGenerateBuiltInPresets()in addition toRefreshPresetManagerLists
The download system is designed to fail gracefully:
- Network timeouts (10s for catalog, 15s for individual downloads) prevent the UI from hanging
- Individual download failures are caught and shown on the button ("Failed") with a status bar message
- Catalog fetch failures show a full error panel with a "Retry" button
- Background fetch (
FetchUcmPresetsAsync) catches all exceptions silently to avoid interrupting startup - Invalid preset content (missing
session_xml,RawXml, orstyle_id) is rejected with an "Invalid" label
| File | Role |
|---|---|
src/UltimateCameraMod.V3/CommunityBrowserDialog.xaml.cs |
Catalog browser dialog, download handler, card rendering |
src/UltimateCameraMod.V3/MainWindow.Community.cs |
Background fetch, GitHub version check, update detection, update download |
src/UltimateCameraMod.V3/MainWindow.Presets.cs |
BuildPresetManagerItems, sidebar list management, ReadCatalogState
|
src/UltimateCameraMod.V3/MainWindow.xaml.cs |
Directory constants, catalog URL constants |
src/UltimateCameraMod.V3/Models/PresetManagerItem.cs |
HasUpdate property binding for sidebar update icons |