openworldformat 0.5.0

The Open World Format Rust reference: the world document, the session fold (log, branches, state), and the ext-physics and ext-cinematography extensions — serde-only, no engine required.
Documentation

openworldformat (crates.io)

The Open World Format's Rust reference: the world document (types and edits), the session fold (log, branches, state), and the ext-physics and ext-cinematography extensions' executable halves — one serde-only crate, no Bevy, no async, no sockets. The same code that implemented the format in LocalGPT since its origin, extracted into the format's own home.

cargo add openworldformat        # or, from this checkout:
cargo add --path rust openworldformat
use openworldformat::{WorldManifest, OpLogEntry, fold_log, fold_path};

// A head-first package: manifest.json is the world now…
let world: WorldManifest = serde_json::from_str(&manifest_text)?;
// …and the log folds over snapshots/base.json back to it.
let base: WorldManifest = serde_json::from_str(&base_text)?;
let entries: Vec<OpLogEntry> = log_text.lines()
    .filter(|l| !l.trim().is_empty())
    .map(serde_json::from_str).collect::<Result<_, _>>()?;

let doc = fold_log(&base.as_base()?, &entries)?;      // the world at head
let (doc, path) = fold_path(&base.as_base()?, &entries, Some("e3"))?; // or at any tip
let same = doc.to_manifest();                          // a whole manifest again

openworldformat::state::fold_state folds the game state over state.json (save games are base + declaration + a player's log); openworldformat::physics carries the extension's deterministic reference solver, trajectory write/fold, and the conformance outcome runner; openworldformat::cinematography carries the ext-cinematography extension's crop math, view and projection, entity origins through parents, aim and focus by entity (view_in, aim_point_of, focus_distance_of), shot list, and outcome runner; openworldformat::ext_h3 carries the ext-h3 geo frame extension's unit table, quantisation, block readers, the tangent-plane derivation of the core transform (local_position, derive_transform) and its outcome runner; the schema feature generates world.schema.json.

0.5.0: pivots, the swing, and the behaviors a reader does not know

Breaking for a consumer that matches BehaviorDef exhaustively or spells out Spin and Pulse: two variants and four fields are new — the compile error is the point, a renderer must decide what it draws for a swing and how it turns a thing about a pivot. The crate's 0.x minor bump, as for 0.4.0.

  • Spin and Pulse gain phase and pivot (spec/world.md "Behavior and triggers"): degrees added at time zero, and the local point — metres in the shape's own frame, before scale — the entity turns or scales about, the origin by default. A wheel is a spin about its base; a thing that breathes on the ground is a pulse about its base. Both fields write nothing at their defaults, so a world written before them reads and writes back byte for byte.

  • BehaviorDef::Swing { axis, amplitude, frequency, phase, pivot }: a pendulum — amplitude × sin(2π × frequency × t + phase) degrees about axis through pivot. A hanging sign, a bell, a seat.

  • BehaviorDef::Other(Value): a behavior kind this version does not know reads as Other, folds through untouched, writes back as it came, and never runs — the must-ignore rule applied to behaviors, where an unknown tag used to refuse the whole manifest. kind() says "unknown"; the schema does not list it. A consumer matching the enum adds an arm for it.

  • MaterialDef::alpha_mode's rule, stated: absent, a colour's alpha below 1 draws blended and 1 draws opaque. The field is the same; the schema's description now says what every renderer must do.

0.4.0: a globe producer's brief, and the geometry table

Breaking for a consumer that matches Shape exhaustively or spells out Author: the new variant and the new field — the compile error is the point, a renderer must decide what it draws for a prism. The spec draft stays 0.3; the versioning policy's alignment is a SHOULD, and a breaking binding change takes its own minor bump under cargo's 0.x rules.

  • The geometry of every shape is stated in spec/world.md and pinned by conformance/geometry/shapes.json; tests/geometry.rs checks that local_aabb_half is the fixture's box for every kind.

  • Shape::Prism { sides, radius, height } (spec/world.md "The geometry of the shapes"): sides 3..=8, radius the circumradius; estimate_triangles counts 4·sides − 4, local_aabb_half is the inscribing cylinder's box. The schema gained the variant; no schema version bump (an additive shape a reader may ignore). Released as experimental; a core shape since other renderers drew it.

  • Author.source — hand, library or ai (AUTHOR_SOURCES), optional, part of the entry's identity; Author::known_source reads an unknown label as none. Author::named builds a plain author.

  • ext_h3 — see above; EXT_H3_KEY, ExtH3Frame, ExtH3Placement and LatLng are re-exported at the root.

  • timestamp_ms is documented as precision-free: a writer may coarsen it to the day.

The 0.3 surface (draft 0.3: head-first, live authoring)

  • The fold is total. WorldDoc carries every manifest field — version, meta, soundtrack, creations, ambience, a next_entity_id that never goes down — and to_manifest writes them all back.
  • EditOp::ModifyWorld { patch: Box<WorldPatch> } reaches meta, environment, camera, avatar, tours, soundtrack, ambience and creations (absent unchanged, null clears); its inverse restores them. A first SetEnvironment/SetCamera now inverts to a clearing ModifyWorld.
  • authoring::ingest(&doc, &batch) — the Authoring profile: names to ids, ids for spawns, partial struct patches merged (merge_patch, RFC 7396), strict reading, whole-batch refusal with a reason per op.
  • manifest_text / manifest_text_of — the canonical text an authority writes, byte-identical to the JS manifestText.
  • OpLogEntry.message (optional) — part of the entry's identity.
  • Head-first packages: PACKAGE_FORMAT_VERSION is 2, BASE_SNAPSHOT is snapshots/base.json; MergeRecord reads the spec's {"merge": {"branch": …}} (it read {"branch": …} before).

The 0.2 surface (draft 0.2 compliance)

The draft-0.2 tightening, as this crate reads it:

  • manifest.version is required — a world that doesn't say which schema it speaks is refused, not defaulted.
  • Entity ids cap at MAX_ENTITY_ID (2^53 − 1): alloc_entity_id returns Result, and the fold refuses ids past the ceiling, so worlds this crate generates never overflow a JSON-safe reader.
  • The v2 multi-file manifest fields (layout_file, region_files, behavior_files, audio_files, avatar_file) are gone from the type, as they already were from the schema.
  • LLM lineage lives under meta["ext-provenance"] (ext_provenance::ExtProvenance) — the ext-provenance extension, not core WorldMeta fields.
  • Names bind at ingestion: WorldDoc::apply_entry (which fold_log uses) resolves Orbit.center/LookAt.target name references against the fold-so-far, and WorldManifest::as_base resolves a base manifest against itself — never at fold time.
  • oplog::canonical_json + oplog::compute_entry_id: the canonical serializer (no whitespace, sorted keys) and the sha256: entry identity computed over it, identical across the five references for integer-valued JSON.
  • EditOp::compute_inverse(&doc): the undo rule in full — a delete's inverse is a batch of spawns holding a deep copy of the deleted tree.
  • session::merge_branch: fork merging that reallocates ids allocated concurrently on the main line and rewrites every reference to them before appending.
  • package::snapshot_filename (snapshots/entry-<id>.json, falling back to rev-<N>.json) and package::compact_plan (base_revision → head; the host performs the file moves).
  • strict::{parse_manifest_strict, decode_line_strict}: must-ignore's off switch for authoring tools — unknown fields, legacy lineage keys and unregistered extensions are refusals, with the extension registry embedded as REGISTERED_EXTENSIONS.

Provenance and the plan

Extracted from LocalGPT's world-types, world-sync and world-physics — not rewritten: in one language, two hand-written type layers would only be drift risk. LocalGPT remains the app-side consumer with its own copies until it's quiet enough to flip them to re-export shims over this crate; until then, both sides generate the same world.schema.json, and the snapshot test in each repository fails when they disagree. The schema's generation_tool default is a pinned literal for the same reason: a published schema must not churn with a generating crate's version.

Two cross-implementation fixes landed with the extraction, applied on both sides: entries may omit author (the spec's own example logs do; the JS fold always read them), and the folded schema is version-independent.

Test

cargo test                      # unit + conformance + examples + extension outcomes
cargo test --features schema    # + the schema snapshot (both generators agree)

CI runs both, alongside the js and python jobs — three references, one set of worlds and outcome assertions.

Apache-2.0.