Skip to content

Add Linux Python CLI port (linux/ucm.py)#32

Open
captainzonks wants to merge 2 commits into
FitzDegenhub:mainfrom
captainzonks:linux-python-port
Open

Add Linux Python CLI port (linux/ucm.py)#32
captainzonks wants to merge 2 commits into
FitzDegenhub:mainfrom
captainzonks:linux-python-port

Conversation

@captainzonks

@captainzonks captainzonks commented Apr 11, 2026

Copy link
Copy Markdown

A note from the contributor

Heads up, the following PR and work was done with Claude Code. I'm a hobby programmer, Claude came out and then allowed my ADHD brain to actually finish ideas and projects. I'm also a Linux gamer, and while I dual boot to Windows for some games, I wanted to get this mod over to Linux, as the performance has hit a good parity and I'd prefer to play Crimson Desert with my Linux boot.

I wanted to acknowledge that AI code can be problematic right now, but that I've managed to successfully use it so far for a lot of projects. The python script for this camera mod works well, I'm playing it now in panorama preset, and I wanted to give it back to the authors and Linux gamers out there.

Thanks for the work, hopefully this also helps the dozens of others who wanted this on Linux <3

— Matt


Summary

Adds a single-file Python port of UCM under a new linux/ subfolder so Linux players (native Steam or Proton) can apply the official style presets without running the WPF app under Wine. The port reuses the existing ucm_presets/ folder at the repo root and ports upstream C# logic module-for-module — no behavior changes on the Windows side.

  • One file: linux/ucm.py (~2,200 lines, pure Python)
  • One dependency: python-lz4
  • Minimum Python: 3.10
  • No GUI, no .NET, no Wine

What's in scope

Matches upstream output byte-for-byte on the camera archive entry:

  • All 7 built-in style presets (Heroic, Panoramic, Close-Up, Low Rider, Knee Cam, Dirt Cam, Survival)
  • Custom distance / height / right-offset / FoV delta
  • Combat pullback, mount-height sync, steadycam, bane/centered
  • Backup / restore / safe re-apply (never compounds on modded files)
  • Full ArchiveWriter.cs size-matching pipeline (scattered comments, inflate strategies, binary search)

What's out of scope

UCM Quick / Fine Tune / God Mode editors, Sacred overrides, JSON / 0.paz / multi-format export, preset catalog browser, live previews, import from mod manager packages. Users who want those stay on the Windows app — this is explicitly the "apply a preset on Linux without Wine" happy path.

Direct C# → Python mapping

Sources under src/UltimateCameraMod.V3/:

C# source Python in linux/ucm.py
Paz/NameHasher.cs compute_hash (Jenkins Lookup3)
Paz/StreamTransform.cs stream_apply (ChaCha20 20 rounds)
Paz/AssetCodec.cs build_codec_params / asset_decode
Paz/PamtReader.cs pamt_parse
Paz/ArchiveWriter.csMatchCompressedSize match_compressed_size + strategies
Services/CameraMod.cs install_with_mod_set + XML engine
Models/CameraRules.cs build_modifications + builders

Only the camera-XML branch of ArchiveWriter is mirrored. The newer MatchCompressedSizeHtml / MatchCompressedSizeCss helpers added for the Center HUD feature target archive 0012 and are out of scope for this Linux CLI port.

Linux conventions

  • Steam auto-detect: native ~/.local/share/Steam, ~/.steam/steam, Flatpak com.valvesoftware.Steam, plus every library listed in each libraryfolders.vdf. --game-dir overrides.
  • Vanilla backup defaults to $XDG_DATA_HOME/ultimate_camera_mod/ (i.e. ~/.local/share/ultimate_camera_mod/) so it never pollutes the repo. --backup-dir overrides.
  • lz4 is lazy-imported so list works without the dep installed.

Test plan

Tested on Arch Linux (kernel 6.19), native Steam install, Python 3.13, python-lz4 4.4.5:

  • python linux/ucm.py list — lists all 7 official presets
  • python linux/ucm.py extract --out /tmp/vanilla.xml — captures clean vanilla XML
  • python linux/ucm.py apply --preset panoramic — installs, size-matching succeeds
  • python linux/ucm.py extract --out /tmp/modded.xml — confirms preset-applied XML
  • python linux/ucm.py restore — restores vanilla
  • diff -q between pre-apply vanilla extract and post-restore extract — byte-identical
  • In-game verification: currently playing with the Panoramic preset applied

Files

  • linux/ucm.py (new) — the port
  • linux/README.md (new) — install, usage, parity notes, troubleshooting
  • .gitignore — ignore ucm_backups/, linux/ucm_backups/, __pycache__/, *.pyc so Python runtime state stays out of the tree

Credit

Full credit to 0xFitz/FitzDegenhub for the original UCM project — this port is a straight mirror of existing C# logic, not a rewrite. The entire upstream credit chain for PAZ/ChaCha20/LZ4 research and camera rule design applies unchanged; see the root README.

Risk / maintenance notes

  • No changes to any existing C# source or Windows-side config — this PR is strictly additive under linux/
  • No CI changes (keeping the first PR small)
  • The port pins to the current .ucmpreset format; if that format changes upstream, load_preset will need a corresponding update

@captainzonks

captainzonks commented Apr 11, 2026

Copy link
Copy Markdown
Author

Just pulled your latest "main," reviewing any necessary changes for this script real quick.

UPDATE: No real changes. Rebasing now and adjusting file paths and some README text.

Single-file Python port of UltimateCameraMod for Linux users who want to
apply presets without running the WPF app under Wine/Proton. Lives under
a new linux/ subdirectory so it stays isolated from the main C# tree.

Scope (matches upstream output byte-for-byte on the camera archive entry):

- All 7 official style presets (Heroic, Panoramic, Close-Up, Low Rider,
  Knee Cam, Dirt Cam, Survival), read from the existing ucm_presets/
  folder at the repo root
- Custom distance/height/right-offset + FoV delta, combat pullback,
  mount-height sync, steadycam toggle, bane/centered
- Backup / restore / safe re-apply semantics (always rebuilds from a
  pristine vanilla snapshot, never compounds on a modded file)
- Ports the camera-XML branch of ArchiveWriter (MatchCompressedSize +
  scattered/inflate/binary-search strategies). The newer HTML/CSS
  size-matching paths added for Center HUD target a different archive
  and are out of scope.

Out of scope (GUI-only upstream features): UCM Quick / Fine Tune / God
Mode, Sacred overrides, JSON / 0.paz / multi-format export, preset
catalog browser, live camera previews, Center HUD.

Direct C# -> Python mapping (sources under src/UltimateCameraMod.V3/):

- Paz/NameHasher.cs            -> compute_hash (Jenkins Lookup3)
- Paz/StreamTransform.cs       -> stream_apply (ChaCha20 20 rounds)
- Paz/AssetCodec.cs            -> build_codec_params / asset_decode
- Paz/PamtReader.cs            -> pamt_parse
- Paz/ArchiveWriter.cs         -> match_compressed_size + strategies
- Services/CameraMod.cs        -> install_with_mod_set, XML engine
- Models/CameraRules.cs        -> build_modifications + builders

Requirements:

- Python >= 3.10
- python-lz4 (only external dep; lazy-imported so `list` works without it)

Steam auto-detect covers native ~/.local/share/Steam, ~/.steam/steam,
and the Flatpak com.valvesoftware.Steam path, plus every library listed
in each libraryfolders.vdf. --game-dir overrides.

Vanilla backup defaults to $XDG_DATA_HOME/ultimate_camera_mod/ (i.e.
~/.local/share/ultimate_camera_mod/) so backups never pollute the repo.
--backup-dir PATH overrides.

Smoke-tested on Arch Linux + native Steam against Crimson Desert:
extract(vanilla) -> apply(panoramic) -> extract -> restore -> extract
yields a byte-identical vanilla XML dump (verified with diff -q).

.gitignore: ignore ucm_backups/, linux/ucm_backups/, __pycache__/, *.pyc
to keep runtime state out of the tree.

Also adds linux/README.md with install instructions per distro, command
examples, auto-detection rules, parity notes, and troubleshooting.

Credit: 0xFitz / FitzDegenhub for the entire upstream project, camera
rule engine, and preset definitions this port mirrors.
Replaces the previous regex-only heuristic in _validate_vanilla() with a
fixture-based check that compares the live camera XML against bundled
SHA-256 hashes of every known-vanilla playercamerapreset.xml. The
fixtures themselves live upstream at docs/vanilla_xml/ and were added
to main in v3.2 (commit 5e59ad0).

Why:

- The old heuristic only spotted UCM's own fingerprint (Fov="40" on
  Player_Basic_Default_Run / Runfast). It would happily accept files
  modded by other tools as "vanilla" and overwrite a legitimate backup.
- Hash matching against a known-good fixture is a stronger signal and
  scales naturally as new game patches land — just hash the new
  fixture under docs/vanilla_xml/ and add it to the dict.

How:

1. _normalize_for_hash() strips any number of leading UTF-8 BOMs and
   normalizes CRLF -> LF before hashing. This is needed because the
   live PAZ entry has 1 BOM + CRLF, while the upstream fixture files
   have 2 BOMs + LF — both decode to the same logical XML, so we
   collapse the byte differences before comparing.
2. _VANILLA_FIXTURE_SHA256 maps version label -> SHA-256 of the
   normalized fixture bytes. SHA-256s are baked into the script as
   constants so the single-file portability promise stays intact —
   the script does not need to read the docs/vanilla_xml/ folder at
   runtime.
3. _validate_vanilla() now:
   - hashes the normalized live XML and looks it up in the dict;
     a hit logs `Vanilla fixture match: <version>` and returns True
   - if no fixture hits but the legacy heuristic still says vanilla,
     logs a fallback WARNING and returns True (lets the script keep
     working on a brand-new game patch before fixtures are baked in)
   - otherwise returns False, refusing to overwrite the backup

Currently bundled vanilla versions:

- v1.02.xx: 2cca7a02587c868ba4e1380fb179444b3ef96bcd25a1019180fc7f591cbd1d0b
- v1.03.00: 6664050d5af09a4d7910e3675dc5d9ca4f0210c8c4ea7a7845e5b7aad9ef330a

Verified end-to-end on Arch Linux + native Steam against the live PAZ
on a v1.03.00 install:

- ensure_backup on a clean game logs `Vanilla fixture match: v1.03.00`
- apply -> restore -> apply round-trip still produces a byte-identical
  vanilla XML extract
- Empty-fixture-dict simulation triggers the heuristic fallback warning
- Modded XML with no fixture hit triggers the refusal path with the
  existing TO FIX message

Adds `import hashlib` to the import block. No new third-party deps.
No CLI changes, no behavior change for users on a recognized game
version — only the safety net is stronger.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant