Skip to content

Add publisher-aware export and protected map content - #84

Draft
Italian-seasoning wants to merge 10 commits into
openOMSI-org:mainfrom
Italian-seasoning:feat/publisher-content-protection-v2
Draft

Italian-seasoning wants to merge 10 commits into
openOMSI-org:mainfrom
Italian-seasoning:feat/publisher-content-protection-v2

Conversation

@Italian-seasoning

@Italian-seasoning Italian-seasoning commented Nov 3, 2026 •

Copy link
Copy Markdown

Replaces #77, which conflicted with main. The branch is the three feature commits plus engine repins, with main (5ceca030) merged in; the latest commits repin the engine, lock its new dependencies and move alias lookups to the map's mounts.

Adds optional publisher packs to openMapper: original-source hashes before conversion, trusted signed provenance, licence metadata and exact dependency references in export, an optional protected pack producer and playback extension, and a bounded opaque OBJ/PPM fingerprint inspector. A second commit adds optional per-buyer placement watermarks for whole maps, and a third builds per-sale packs and licences. Ordinary maps and exports keep their current behaviour and need no account or hosted service.

Engine dependency

Requires openOMSI-org/openOMSI#3593 at its head, 18567a01a17e24de9a2566c13801557ac7fc72a3. All 13 manifest pins and the 27 engine entries in Cargo.lock select that revision on the contributor fork; Cargo.lock also gains the three dependencies that revision adds (windows for omsi-pack, wgpu-hal, windows-core). After openOMSI-org/openOMSI#3593 merges, the pins and the format link in docs/PUBLISHER_PACKS.md should move to an openOMSI-org commit.

Review corrections included

  • Protected SCO outputs cannot leave export through an invalid alias, and a refused export leaves no partial ZIP.
  • Protected maps cannot be loaded through MCP or hosted as collaboration sessions.
  • ZIP files, installed files and dependency manifests are written through unique same-directory temporaries, so stale temporaries no longer block export.
  • Engine side: untrusted catalog entries are isolated, alias scopes bind the full selected pack revisions, signed scripts and varlists load, absent Keychain/DPAPI grants report exactly, mapgen disk caches are bypassed after an encrypted pack is mounted (multiplayer session stores keep the cache), and retained PackIndex history no longer blocks export.
  • The editor resolves publisher aliases through the map's own dependency mounts (ec8002a), as the game has since Add optional signed publisher packs and protected playback openOMSI#3593 dropped the global alias lookup in resolve_path. Asset loading, the map check, the ground texture swatches and MCP use it; without it the editor loaded no object from a pack and the map check reported each one as missing.
  • The MCP server moved into src/mcp/ on main; the refusal of publisher-protected maps moved with it (mcp/mod.rs), and its test now calls every tool the server lists.

Optional placement watermarks (second commit)

tools/placement_watermark.py gives each buyer's copy of a map its own object placements: eligible [object] records in the tile files move horizontally by 0.3 mm (0.1 to 0.5 mm allowed), in directions derived from the creator's secret key and an opaque buyer ID. Objects that roads, rails or AI paths connect to, attached objects, splines, Chrono tiles and all other files stay unchanged. inspect compares a suspect copy with the unmarked original and reports each candidate ID's score and an upper bound on the false-match probability. It contacts no service and stores no buyer data. Limits, including that deliberate millimetre jitter defeats it on small maps, are in docs/PLACEMENT_WATERMARKS.md. It is kept in its own commit so it can be reviewed or dropped separately.

Per-sale packs and licences (third commit)

openmapper --publish-licensed-packs spec.json sales signing.pk8 licence-ids.txt <expiry-unix> builds one protected pack and one licence for each opaque licence ID in the list. Each pack has a fresh content key, and its signed manifest names the ID through the engine's new licence_id; the licence repeats it, so one buyer's licence does not unlock another buyer's pack. Licence IDs use the placement watermark buyer ID format, so one ID can name both, and they hold no personal data. The buyer receives <id>.oopp and <id>.licence.json. The publisher keeps <id>.key, for replacement licences, and licences.json, which records each ID's pack, version, manifest digest, key ID and expiry; both are written owner-only. An unprotected specification, a repeated, malformed or case-only duplicate ID, a past expiry or any build failure leaves no output. Original-file hashes are the same in every buyer's pack, so provenance recognition still works.

Validation

  • Windows 11 (x64), openMapper at ec8002a with the engine at 18567a01: cargo test --release --locked 620 passed, 2 failed, 7 ignored. The two failures, installed_maps_survive_a_save and stock_depot_reads_back_the_same, read the add-on maps installed on that machine (Cayuga, Habana Oeste) and fail the same way on main (5ceca030) there; this branch does not touch them. 93e74ab only removes an unused mut on Windows; cargo test --release --locked publisher passes there (5).
  • Before ec8002a, the repin alone failed four publisher tests on Windows (alias scope, signed aliases, both export round trips); they pass at the previous head (774932f, engine ca01f9bb) and again now.
  • cargo build --release on Windows passes. Translation check (2,736 texts, 26 languages), fingerprint tools and placement watermark tests pass (macOS).
  • Not rerun on macOS at this head: the macOS and Linux jobs are left to CI.
  • The command-line wrapper for per-sale publication was not run as a separate process; the test calls the same function.

Contributor-fork CI: the Content protection run for 93e74ab is queued. On the previous head its macOS job failed one test that is not part of this branch, a_set_of_pbr_pictures_becomes_one_material_with_openomsis_maps: import.rs on main treats a normal map as OpenGL-style when its whole path contains "gl", and the macOS temporary folder name is random. No gameplay, hardware or extraction-proof acceptance is claimed.

Signatures identify trusted publishers, not ownership. A modified player can capture usable assets, and offline grants cannot guarantee immediate revocation.

🤖 Generated with Claude Code

Optional publisher packs for openMapper, built on the engine's omsi-pack
and omsi-cfg. Ordinary maps and exports keep their current behaviour.

- Hash original source bytes before conversion and recognise trusted
  signed provenance after renames, including retained PackIndex history.
- Export restricted assets as exact dependency references in
  dependencies.json instead of copies, with ordinary warnings and an
  explicit redistribution-rights override. Protected content has no
  plaintext export path, including SCO outputs under invalid aliases.
- Keep publisher-protected maps playback-only: no save, autosave, MCP
  exposure or collaboration hosting of protected sources.
- Write ZIP, installed files and dependency manifests through unique
  same-directory temporaries that are removed on failure.
- Publisher authoring: signed packs with optional AES-256-GCM content.
- Optional bounded OBJ/P6 PPM fingerprint tool with documented limits.
- Pin the engine crates to the content-protection engine revision.
@Italian-seasoning
Italian-seasoning force-pushed the feat/publisher-content-protection-v2 branch from 2a80497 to 0a4682d Compare November 3, 2026 23:22
tools/placement_watermark.py gives each buyer's copy of a map its own
sub-millimetre object placements, derived from the creator's secret key
and an opaque buyer ID, and inspects a suspect copy against the unmarked
original with a stated false-match bound.

- Only x and y of eligible [object] records in top-level tile files
  move, by 0.3 mm by default (0.1 to 0.5 mm allowed). Objects with
  paths, crossings or entry points, attached objects, splines, Chrono
  tiles and all other files are copied unchanged.
- Follows each tile's [version] layout. The marked copy is written to a
  new folder that appears only when complete; inputs are never changed.
- Tests cover exact, partial, reordered, rounded, averaged and mixed
  copies, 10,000 wrong IDs, the jitter limit and unsafe inputs. Both
  CI workflows run them.
openmapper --publish-licensed-packs builds one protected pack and one
licence for each opaque licence ID in a list, using the engine's new
signed licence_id (openOMSI f10c6d68).

- Each pack has a fresh content key, and its signed manifest names the
  licence ID. Its licence repeats the ID, so one buyer's licence does
  not unlock another buyer's pack.
- Licence IDs use the placement watermark buyer ID format, so one ID
  can name both. They hold no personal data.
- The buyer receives <id>.oopp and <id>.licence.json. The publisher
  keeps <id>.key, for replacement licences, and licences.json, which
  records each ID with its pack, version, manifest digest, key ID and
  expiry. Both are written owner-only.
- An unprotected specification, a repeated, malformed or case-only
  duplicate ID, a past expiry or any build failure leaves no output.
- Original-file hashes are the same in every buyer's pack, so
  provenance recognition still works.
- Repin openOMSI 6ac0681e -> f10c6d68.
Italian-seasoning and others added 2 commits November 4, 2026 01:32
Move the engine pins from f10c6d68 to 640f2ea1, which reports a missing
Windows entitlement as KeyNotFound and moves entitlement code into its
own module. No editor code changes.
Locale files keep both sides' new strings. The publisher-protected save
guard now sits in save_with, so the background save is refused too, and
the background copy of the document carries the publisher mounts and the
protected flag. The openOMSI pin moves to the synced openOMSI branch.
@Italian-seasoning
Italian-seasoning marked this pull request as ready for review November 5, 2026 04:10
@Italian-seasoning
Italian-seasoning marked this pull request as draft November 5, 2026 07:39
@Italian-seasoning
Italian-seasoning force-pushed the feat/publisher-content-protection-v2 branch from e731d78 to 774932f Compare November 8, 2026 21:29
The MCP server moved into src/mcp/: load_document and the refusal of a publisher-protected map now sit in mcp/mod.rs, and the test covers every tool the server lists.
openOMSI no longer resolves a publisher alias through the global resolve_path
(30fadba8): the game asks the map's DependencyMounts. The editor now does the
same. Assets keeps the mounts with its map scope, Document::resolve_alias
answers for the folder the map was read from, and the map check, the ground
texture swatches and MCP use it. Without this the editor loaded no object
from a pack and the check reported every one as missing.
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