Repository navigation
Releases: OnlyWorlds/unity-sdk
Release list
OnlyWorlds Unity SDK 0.9.0
Party links: one join link seats a table of players, each picking a Character from the link's list (or typing a name
at a table that lists none), walked live on onlyworlds.com through a game's door; the wire check now reads request
and response bodies and error codes; the schema pin moves to dist.35. A minor release because it adds public API
(below).
Added
- Party links (one join link for a table of up to 50 players):
OWJoinPreviewreadsMaxUses,UsesLeft,
NameRequiredandCharacters(OWJoinPick: id, name, image url, taken), andIsTabletells a party link from a
link for one, which sends none of them. OWJoin.RedeemAsynctakescharacter, the player's pick from the preview's list. It is the last parameter, so
existing positional calls keep working.OWApiError.IsCharacterTaken,IsCharacterInvalidandIsWorldFull, matching the npm SDK's helpers.
Changed
- The schema pin moves to dist.35 (
v0.32.2-dist.35; canonical00.32.02unchanged). The portal's example download,
vendored for the envelope tests, now carriesworld.sourceandworld.exported_asas keel writes them, and the
reader and the file export keep both. The generated models changed only in their serial. - The wire check also compares request body keys, response keys and error codes with keel's OpenAPI document
(tools/refresh_wire.py; the fixture refreshed from production). The SDK's join keys, the preview's and a join's
answer keys, and every code anOWApiErrorhelper names are checked against what keel declares.
OnlyWorlds Unity SDK 0.8.0
A world folder zipped as one .owworld file imports into Unity as a read-only world cache, measured in the Editor, a
desktop player, an Android IL2CPP player and a WebGL player; the calendar reader follows the new atlas_calendar
profile and its conformance fixture; the schema pin moves to dist.33. A minor release because it adds public API
(below).
Added
- World archives,
.owworld(a spec world zip, renamed: one world, as one top-level folder or withworld.json
at the root). Drop one intoAssets/and it imports as a read-onlyOWWorldCache; a zip with no world or two is an
import error, never a guess. The cache is keyed by the world's id and the archive asset's GUID
(OWWorldArchive.KeyFor(worldId, guid)), so the asset carries no machine's path and keeps its key through a move.
The archive is read in memory, never extracted, and an entry name that is absolute or climbs out of the world
refuses the archive.Assets/OnlyWorlds/Pack World Folder...packs a folder from anywhere
on disk into the project and imports it;OWWorldArchive.Pack(folder, archive)is the same step from code. Pack
writes only the archive, never into the folder; packsworld.json,elements/,spatial/andmedia/, leaving out
dot-folders, dot-files and Unity.metafiles, andrecords/whole and byte for byte (folder spec §4.5: a producer's
files are never dropped; the importer reads none of them); dropsworld.json'sapiblock (it holds a key) unless asked to
keep it; and is deterministic (ordinal entry order, a fixed timestamp).OWWorldArchive.Read(archive)gives a folder
read's result. Media travels in the archive but is not imported yet. OWFolderReader.ReadEntries(entries, includeLegacySpatial?, source?)reads a world held in memory, as
(path inside the world, text) pairs, by the folder read's own rules and code;Read(folder)is unchanged.OWFolderLoader.LoadInto(cache, read, key, readOnly?)fills a cache from a read already done, so it is not paid
twice.LoadInto(cache, path)now calls it and behaves as before.- Measured on Tuvsh Ul (3,829 elements): an imported archive loads about 5-6x faster than the folder reader from
cold in a release desktop player, and the asset and a runtimeOWWorldArchive.LoadIntoboth work in an Android
IL2CPP player and a WebGL player (Minimal stripping; higher stripping untested).
Changed
OWJoin.ParseLinknames the link shape with the preview on,<site>/join/<world_id>#<code>(keel #422), and a
test holds it. Its behaviour is unchanged: it always read the code after the last#.- The schema pin moves from dist.24 to
v0.32.2-dist.33(canonical00.32.02): two descriptions in the generated
models (an Event'scharactersare those who know of it; a Narrative's dates are when it is told), and
ow-presentation.jsongains each type'spubliclist (the fields shown on a known-of element). The walk and the
rulings are unchanged.
Fixed
- The calendar reader follows the
atlas_calendarprofile (schema-distprofiles/calendar.md, dist.33) and the
TypeScript kernel 4.42.2, checked by the profile's conformance fixture, now vendored (OWCalendarFixtureTests: 52
cases, 113 steps). A down era counts its years from the one holding the step before its pivot, so a mid-year pivot
has no year 0, andParseDateis its inverse again; at equal starts an up era wins over a down one, then the later
in the list; anerasorintercalarythat is present but not a list (false,0,"") is not a calendar; a
calendar written as a JSON string is read only when it is strict JSON.
OnlyWorlds Unity SDK 0.7.0
Join a world by link, read the one-file export and write it as a folder, and an export's world.json says where it
came from and which slice it is (folder format v0.3.9). A minor release because it adds public API (below).
Added
- Joining a world by link (keel #212, #353):
OWJoin.ParseLink(text)finds theow_j_code in a pasted link or
a bare code;OWJoin.PreviewAsync(code, transport)(POST/join/preview) says who invites, to which world, for
which reader, without spending the code;OWJoin.RedeemAsync(code, transport, name?, agentName?)(POST/join)
spends it. A player's code answers with a read key (ow_r_, no PIN) and the Character it reads as; an agent's with
a seat's write key and secret. Both send no credential, so they sit besideOWClient, which cannot be built
without a key; npm'spreviewJoinandredeemJoinare the same wire. Every bad code is keel's one 404
not_found(OWApiError.IsNotFound). The wire check records both calls (12 now). - The one-file world export (
format: onlyworlds-world-export, spec §8c), the file an owner downloads from a
world's page:OWEnvelopeReader.Read(path)/Parse(json)give a folder read's result (OWEnvelopeReadResult,
with the file'sFormat,SchemaVersionandExportedAt), andOWEnvelopeLoader.LoadInto(cache, path)fills a
read-only cache. The reader keys onformatalone;schema_version(the standard's version the elements follow)
is kept as information, never checked. An element with no id is skipped and reported, as in a folder. The test
fixture is keel's own emitter output, published in schema-dist (example/world-export.json, serial 25) and vendored
byte for byte; a test holds its hash to the dist's MANIFEST. - The one-file export as a world folder:
OWFolderExport.ExportFileAsync(path, folder, options?, progress?, ct?)
writes the downloaded file as the same folderExportAsyncwrites from the API (bodies as the file holds them, every
key of its world carried intoworld.json, each Map's image fetched from itsimage_url), born whole or not at all.
What a folder cannot keep whole is refused, not skipped: an element with no id, a type that is not the standard's,
one id under two types. - The provenance lines (folder format v0.3.9, §4.2): an API export's
world.jsonnow carriessource
({server, world, at}: the API's origin, the world's id, when the export began, astoISOStringwrites it) and,
whenGET /mesays which slice the key reads,exported_as({kind, role, character}), after the wire's keys, as
the Python package'sexport_worldwrites them (0.7.0).OWFolderExport.ExportedAs(me, characterName)is the
spec owner's table in one place; a refused/me, or a guest whose Character cannot be read, writes none. New:
OWClient.GetMeAsync(GET/me/, in the wire check: 13 calls),OWFolderExportOptions.Clock.
Changed
- An export declares folder format
0.3.9(OWFolderExport.FormatVersion), as the package's 0.7.0 does.
OnlyWorlds Unity SDK 0.6.0
A world folder that Atlas wrote now opens whole: its pins' and markers' links, and the older spatial/ layout.
A minor release because it adds public API (below).
Fixed
- A world folder's pins and markers keep their links when Atlas spelled them
map_idandzone_id. The spec
says a reader accepts both spellings, and Atlas writes the_idforms.OWFolderLoader.LoadIntogave the cache the
body as written, so a pin from an Atlas folder had noMapand a marker noMaporZone. The cache now gets
OWFolderReader.WithSchemaLinkNames(body): the schema's name, in the old one's place, when the schema's name is not
already there. The read result's bodies and the files on disk are unchanged. - One id, one file (folder spec v0.3.8 §4). Where an older Atlas folder holds one id in both
elements/and
spatial/,OWFolderReader.Readserveselements/and logs thespatial/copy inSkipped(it used to be loaded
last, so the older copy won in the cache).OWFolderWriter.WriteElementwrites a map, pin, zone or marker that
lives underspatial/<type>/there, in its own file, so an edit never leaves a second copy; a new element goes to
elements/.OWFolderExportstampsformat_version"0.3.8", as the Python package does since its 0.5.0.
Added
OWFolderReader.WithSchemaLinkNames(body): a body's links under the schema's names (map_idasmap,zone_id
aszone), a copy when anything differs; what the cache is given.OWFolderReadResult.TimeCurrent:time_current, else the wire'stime_range_current(both accepted, as the
Python package reads them).OWFolderReadResult.ServerWorldId: a linked folder'sapi.world_id, else itsid.
WorldIdstays the folder's own id, which keys the cache.- The reader's notes name the folder spec it follows: v0.3.8's reading rules (they said v0.3.5).
OnlyWorlds Unity SDK 0.5.0
Added
- World calendars:
OWCalendarreads a world's calendar (theatlas_calendarfield Atlas writes on a calendar
Construct) and formats, breaks down and parses dates on the world's integer time line: eras, months, weeks, leap and
intercalary days. It is a port of the npm SDK's calendar kernel (@onlyworlds/sdk4.23.0), checked against the same
test vectors, so a date reads the same in Unity as in Atlas and Lantern.OWCalendarLens.WorldCalendarsand
ResolveLensfind the calendars a world holds. A missing or malformed calendar reads as null, never an error. - The door of every write (OnlyWorlds keel #299; the npm SDK's 4.26.0 has the same).
Agent = trueon
OWClientConfigsendsX-OW-Agenton every element write (create, patch, delete, link edit, bulk), never on a read,
and the server records those writes as agent-made. It is off by default and belongs only in a client an agent
drives. - Every element reads the server-managed
ChangedBy(the membership whose write last changed it) andChangedVia(that
write's door:seat,mcp,declared,api,v1,syncorserver). A/changesdelete readsDeletedByand
DeletedVia.OWWriteDoor.IsAgent(door)is true forseat,mcpanddeclared. OWApiError.RetryAfter: the seconds to wait from the response'sRetry-Afterheader (a delay, or an HTTP date
turned into one), or null. The server sends it on 503server_busy, 429 and 409idempotency_in_flight. It is read
as the npm SDK reads it, and the client still never retries on its own.- Export an account world to a world folder:
OWFolderExport.ExportAsync(client, folder)reads the world and all 22
element types (GETs only) and writes the folder the onlyworlds Python package'sexport_worldwrites, a port of its
rules: element files byte for byte, the collision rule's id-ascending names,time_currenton disk,
format_version"0.3.7". On Moppetopia's 310 elements it matched the package's output on the same wire bodies
with no file different. Each Map's image is kept as Atlas keeps local media (media/<map id>/base-image/, as served,
with its.meta.jsonsidecar); an image that will not fetch is a line in the report, never a failure. The folder
is built beside its destination and moved into place, so a cancel or an error leaves no half-written world, and it
must not exist or be empty.OWFolderExport.WorldForDiskis theworld.jsonstep on its own. The key and PIN go
into no file and no message. On Windows a folder too deep for the files a world needs is refused before the first
write: Unity's Mono does not take long paths.
Changed
- Schema pin moved to
v0.32.0-dist.24(fromdist.20): canonical unchanged. The elementidis described as a
UUID, new ids minted as v7 and earlier v4 ids still valid. The generated models changed only in their stamps. - New ids are UUIDv7, as the standard now says and as the server, the npm SDK (4.2.0 on) and the Python package
mint them:CreateAsyncandBulkAsyncmint one withOWId.NewId()when an element has no id and no key is
given, where they used a v4Guid. Ids this client mints a millisecond or more apart sort by creation; a world still
holds v4 ids, which stay valid, so order elements bycreated_at, never by id. A caller-supplied id is still sent as
given. - Faster folder reads:
OWFolderReader.Read(and soOWFolderLoader.LoadInto) reads a world's files on up to 8
threads, then parses them one at a time in the same order as before. On a 20,570-element world it took about 2.0 s
instead of 3.1 s, with the same elements, order, types and skipped files. Parsing stays on one thread on purpose: under
Unity's Mono GC a parallel parse measured slower. WebGL players read one file at a time.
Fixed
- The folder writer escapes strings as
JSON.stringifydoes.OWFolderWriter.Serializewrote U+2028, U+2029 and
U+0085 as escapes, in values and in keys, where the reference implementation and the Python package write them
raw, so a name holding one was a byte difference in every folder. It now matches the package's bytes on a body
holding them, the short escapes, the other controls and a surrogate pair. A lone surrogate is written as its escape. OWFolderWriter.Writesettles a collision id-ascending, as the spec's writer rule requires: a batch is written
lowest id first, so of two elements whose short filenames match (compared case-insensitively, on every filesystem)
the lower id keeps the short form whatever order they came in. Into an empty folder it now names files exactly as
the Python package does. Every body of a batch is checked before the first is written, and a refused batch writes
nothing.WriteElementstill never renames an element already in the folder.ChangesAsyncpages the feed. It sent its cursor ascursor=, and the changes feed takessince, refusing any
other parameter with a 422. SoOWSync.IncrementalAsyncfailed whenever a world had changed since its baseline. It
now sendssince.- A wire check against the API's own document.
OWWireContractTestsrecords every requestOWClientcan make and
checks each against a snapshot of the server's published OpenAPI document: a path it serves, a method it takes
there, and only query parameters and headers it declares. It catches both bugs above, which had passed their own
tests since 0.1.0.tools/refresh_wire.pyrefreshes the snapshot (--checksays whether the server's document has
moved). EditLinksAsyncreaches the server. Since 0.1.0 it posted to/{type}/{id}/editLinks/, a route the API never
served, so every call failed with 404. It now postsaddandremoveto/{type}/{id}/links/{field}. Its test had
asserted the wrong URL; it now asserts the server's.- The folder writer's round-trip tests run in any host: they find their fixture through the package's own resolved
path, so a project that lists the SDK intestablesand references it byfile:from outside its own folder runs
all three instead of skipping them. CreateAsyncsendsIdempotency-Keyonly when you pass one, as the npm SDK does. Since 0.1.0 it sent the
element's id as the key on every create. The server keeps a keyed create's first answer for 24 hours and answers an
identical repeat with it, writing nothing, so re-creating a deleted element under its old id (an undo) got the old
201 back and the element stayed deleted. A retry without a key is safe when the element carries its own id: it
answers 409id_conflict, and the element is there. An element without one gets a fresh id on every call, so an
unkeyed retry of it creates a second element, as in the npm SDK and the Python package; a caller who retries passes
idempotencyKey, and the stored answer is replayed.- A keyed retry replays. With an
idempotencyKeyand no id,CreateAsyncandBulkAsyncminted a fresh id on
every call, so a retry sent a different body and the server answered 409idempotency_errorinstead of its stored
answer. The id is now derived from the key and the element's position (OWId.Keyed, a UUIDv5 in the Python
package's namespace), so a retry sends the same body. The Python package and the npm SDK (4.27.1) derive the same
id, so a key mints one id in all three clients. Without a key, ids are minted as before. OWSync.IncrementalAsyncresumes from the server's own cursor, kept as is in the cache's newCursorToken,
where it built one from the lastchange_seq. The server answers a guest key's bare number with 409
resync_required, so a guest key's incremental sync failed on its first real change. A cache synced by an earlier
version has no token and resumes from itschange_seq, as before. A 409resync_requirednow rebuilds the cache
from the start, with aRebaselineReasonsaying why: a guest is never sent deletes, so only a rebuild drops what it
can no longer see.- The sync reads the feed's
epoch(OWChangesPage.Epoch, kept asOWWorldCache.Epoch). A guest's view can
change with the head unmoved, so a cache at the head is current only when the epoch is unchanged too, and
HasChangesAsyncsays so. - Each upsert is applied from the element it carries (
OWChange.Element), where the sync fetched every changed
element again: one request per page instead of one per change. If the feed reports more pages but gives no cursor,
the cache keeps itschange_seqand the last cursor the server gave, where it recorded the highestchange_seqit
had read; a bulk import gives many changes onechange_seq, so that number could claim changes never read. An
empty world's cache no longer rebuilds on every sync. ChangesAsyncsendslimit: the client'sPageSize, at most 1000, above which the feed answers 422. The
server's default page was 500; the npm SDK asks for 100.OWApiError.IsRetryablereads the error code, where it was any 429 or 5xx. A 409idempotency_in_flight(the
same key still running: send it again and it replays the first answer) now reads as retryable. A 429
quota_exceeded(a daily allowance, used up until midnight UTC) and a 503ask_unavailableormedia_unavailable
(a feature switched off) no longer do.OWApiError.IsAuthErrorreads the error code, as the npm SDK does:invalid_credentials,key_revoked(and
the reservedworld_gone), where it was any 401 or 403. A 403 such asnot_authororowner_onlycomes with a
valid key, and the World Browser no longer tells you to check the key and PIN for one: it says the element is
someone else's, that only the owner can do this, or that the key's scope does not allow it.IsNotFoundis a 404
with the codenot_found, so a proxy's 404 is not mistaken for a missing element. New:IsNotAuthor,
IsOwnerOnly, andIsResyncRequired.- **`OWKey.CanWrite(OWKeyK...
OnlyWorlds Unity SDK 0.4.0
Added
- List filters:
OWListParams.Name,NameContains,SupertypeandSubtypesend the API'sname,
name__icontains,supertypeandsubtypefilters.ListAllAsyncnow carries every filter onto every page.
Fixed
- The World Browser no longer demands a PIN to connect. Prefixed keys read without one, and a legacy key needs one
only for writes or a private world, so the demo keys that worked in code were locked out of the browser. The
settings window now says when a PIN is needed for each kind of key. - The editor's key, PIN and base URL are kept per project. They lived in one machine-wide EditorPrefs entry,
so every project on the machine shared one key, and a key rotated for one world went stale in all the others.
They are now scoped by the project'sPlayerSettings.productGUID(clones and worktrees of one project share
them), still in EditorPrefs, still never in the project. A project with no key of its own offers the
machine-wide key an earlier version saved ("Use it here" in the settings window); it is never read silently. - The World Browser loads the whole world on connect, in the background, so every type's count and every
link's name show before a type is clicked; before, a link read as a raw id and a count stayed blank until its
type was opened. A type clicked during the load still opens at once. Unnamed markers read as their zone and
order (" · 3") instead of "(unnamed)", and type names are capitalised. - The World Browser shows a folder world or a cache with no key. A key is needed to reach the API, not to
read what is already on disk; with none set, the browser showed only the setup prompt, even over an open
folder world, which needs no account at all.
Changed
- Schema pin moved to
v0.32.0-dist.20(fromdist.15, by way ofdist.18anddist.19): canonical
00.32.00. The World gainslength_unit,mass_unitanddistance_unit(the world units) andcreated_at,
updated_atandpublic_read, and losesapi_key,userandversionfrom its definition;GetWorldAsync
returns the World as aJObject, so all of it arrives with no code change (world["length_unit"]). The element models changed only in
their doc comments: the unit fields now name the World field they are measured in. Inpresentation.json,
Zone's icon is nowpentagonand Marker'spolyline, and the World Browser draws both. - The World Browser reads like an editor list: the types in alphabetical order, each with its icon in its
family's colour and its count; selectable rows with a hover state in place of button columns; the element's
name, type, supertype and subtype as a header; and a link field shows the linked element's name, which opens
it. A link resolves to any element the window has loaded, which, with the world loaded on connect, is every
element; an id outside the world stays an id. The family legend is gone: the colours stay, the grouping is not shown. The 22 icons are Google's Material
Symbols (Apache 2.0, licence inEditor/Icons/), rendered bytools/render_type_icons.pyfrom the names in
ow-presentation.json. - The READMEs open with a first run that needs no account: the demo key
0000000001reads Moppetopia. Agent seats
lead for writes. The account token is no longer offered to the client.
OnlyWorlds Unity SDK 0.3.0
All 22 OnlyWorlds element types as typed C# models, generated from the schema distribution.
- Models for every element type, generated by
codegen/generate_models.py;codegen/check_drift.pyfails when a committed model no longer matches a fresh generation. OWFolderWriter: writes a world folder to disk in the bytes the folder format specifies (LF, UTF-8 without a BOM, two-space indent, keys in their original order), so the same world writes the same files on every machine.OWFolderWriter.Parsereads JSON without rewriting timestamps.OWElement.CreatedBy: who created an element on a shared world.- Writes now send
""for an unset string field, matching the API.
Install from the Package Manager with + → Add package from git URL:
https://github.com/OnlyWorlds/unity-sdk.git?path=/Packages/com.onlyworlds.sdk#v0.3.0
Full list: CHANGELOG.
OnlyWorlds Unity SDK 0.2.0
Install via Package Manager -> Add package from git URL:
https://github.com/OnlyWorlds/unity-sdk.git?path=/Packages/com.onlyworlds.sdk
Reading worlds from disk, writing them back safely, and a guard on the vendored schema. Verified
against a 142-test EditMode suite and, for the first time, a real stripped IL2CPP build.
Added
OWFolderReader/OWFolderLoader— read an OnlyWorlds world folder from disk (spec v0.3.5)
and load it into a cache keyed(Folder, worldId, folderPath). Reading is not editing: no
directories, dot-folders or marker files are created, and no file is ever rewritten — including to
normalise a key spelling. Files that cannot be used are reported, never deleted; one malformed
file never costs you the rest of the world. Verified against a second implementation's
conformance fixture.OWBrowserWindow→ Open Folder… — the reader, reachable.OWEdit— typed edits that PATCH only what changed. Diffs a baseline snapshot, so it needs
nothing from the model and works for generated types that do not exist yet. Sends nothing when
nothing changed, because a no-op PATCH still bumpsupdated_atand shows up as a phantom edit in
everyone else's change feed.OWClient.BulkAsyncwithOWBulkResult— bulk writes whose partial failures are hard to
ignore. A bulk request returns HTTP 200 even when individual slots failed, so the per-slot
status is the only place the truth lives.OWSchemaPin+ drift guard — the vendored presentation sidecar is hash-checked against the
pinned distribution on every test run. The pin is(tag, manifest hash), never the tag alone: a
tag is mutable, and moving one regenerates a self-consistent manifest that cannot detect its own
tag having moved.
Fixed
- Extension fields survived JSON but not Unity's own serializer.
OWElementdid not implement
ISerializationCallbackReceiver, so the methods carrying the bag across a Unity serialization
pass were never called. An element held in aMonoBehaviourorScriptableObjectfield lost
everyx_*field. - Every field was serialized twice —
nameandName. Newtonsoft defaults to OptOut, so
public properties went to the wire beside their attributed fields. The server ignores the
PascalCase copies, which is why it survived a day of green tests. - Incremental sync could advance its cursor past changes it never applied, if the changes feed
reported more pages but gave no cursor to reach them — silently losing them forever. - The rewind diagnostic reported the wrong cursor, rendering "Cursor 100 was ahead of server
head 100". The value it existed to report was overwritten before the message was built. OWMainThread.Runqueued work into silence when no pump existed: the task never completed and
never faulted, hanging the caller with an empty console. It throws now, and says what to do.OWWorldCache.Upsertleft an element in its old type bucket when its type changed — reachable
by the folder reader on the first file whose body contradicts its directory.HasChangesAsync's two-directional truth is documented rather than surprising.
Validated
- The nullable converter survives IL2CPP stripping. Against a real stripped Android build: five
closedSerializableNullable<T>instantiations, three closed converter instantiations, the
factory reflectable inglobal-metadata.dat, andReadJson/WriteJsonas executable methods.
Runtime behaviour on device is still unproven — the probe's verdict only prints on hardware.