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.
use ;
// A head-first package: manifest.json is the world now…
let world: WorldManifest = from_str?;
// …and the log folds over snapshots/base.json back to it.
let base: WorldManifest = from_str?;
let entries: = log_text.lines
.filter
.map.?;
let doc = fold_log?; // the world at head
let = fold_path?; // 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.
-
SpinandPulsegainphaseandpivot(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 aboutaxisthroughpivot. A hanging sign, a bell, a seat. -
BehaviorDef::Other(Value): a behavior kind this version does not know reads asOther, 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.rschecks thatlocal_aabb_halfis the fixture's box for every kind. -
Shape::Prism { sides, radius, height }(spec/world.md "The geometry of the shapes"):sides3..=8,radiusthe circumradius;estimate_trianglescounts4·sides − 4,local_aabb_halfis 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,libraryorai(AUTHOR_SOURCES), optional, part of the entry's identity;Author::known_sourcereads an unknown label as none.Author::namedbuilds a plain author. -
ext_h3— see above;EXT_H3_KEY,ExtH3Frame,ExtH3PlacementandLatLngare re-exported at the root. -
timestamp_msis 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.
WorldDoccarries every manifest field — version, meta, soundtrack, creations, ambience, anext_entity_idthat never goes down — andto_manifestwrites them all back. EditOp::ModifyWorld { patch: Box<WorldPatch> }reaches meta, environment, camera, avatar, tours, soundtrack, ambience and creations (absent unchanged,nullclears); its inverse restores them. A firstSetEnvironment/SetCameranow inverts to a clearingModifyWorld.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 JSmanifestText.OpLogEntry.message(optional) — part of the entry's identity.- Head-first packages:
PACKAGE_FORMAT_VERSIONis 2,BASE_SNAPSHOTissnapshots/base.json;MergeRecordreads 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.versionis 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_idreturnsResult, 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) — theext-provenanceextension, not coreWorldMetafields. - Names bind at ingestion:
WorldDoc::apply_entry(whichfold_loguses) resolvesOrbit.center/LookAt.targetname references against the fold-so-far, andWorldManifest::as_baseresolves a base manifest against itself — never at fold time. oplog::canonical_json+oplog::compute_entry_id: the canonical serializer (no whitespace, sorted keys) and thesha256: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 torev-<N>.json) andpackage::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 asREGISTERED_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
CI runs both, alongside the js and python jobs — three references,
one set of worlds and outcome assertions.
Apache-2.0.