Skip to content

OnlyWorlds Unity SDK 0.5.0

Choose a tag to compare

@tmoosting tmoosting released this 16 Oct 13:52
· 24 commits to main since this release

Added

  • World calendars: OWCalendar reads a world's calendar (the atlas_calendar field 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/sdk 4.23.0), checked against the same
    test vectors, so a date reads the same in Unity as in Atlas and Lantern. OWCalendarLens.WorldCalendars and
    ResolveLens find 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 = true on
    OWClientConfig sends X-OW-Agent on 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) and ChangedVia (that
    write's door: seat, mcp, declared, api, v1, sync or server). A /changes delete reads DeletedBy and
    DeletedVia. OWWriteDoor.IsAgent(door) is true for seat, mcp and declared.
  • OWApiError.RetryAfter: the seconds to wait from the response's Retry-After header (a delay, or an HTTP date
    turned into one), or null. The server sends it on 503 server_busy, 429 and 409 idempotency_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's export_world writes, a port of its
    rules: element files byte for byte, the collision rule's id-ascending names, time_current on 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.json sidecar); 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.WorldForDisk is the world.json step 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 (from dist.20): canonical unchanged. The element id is 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: CreateAsync and BulkAsync mint one with OWId.NewId() when an element has no id and no key is
    given, where they used a v4 Guid. Ids this client mints a millisecond or more apart sort by creation; a world still
    holds v4 ids, which stay valid, so order elements by created_at, never by id. A caller-supplied id is still sent as
    given.
  • Faster folder reads: OWFolderReader.Read (and so OWFolderLoader.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.stringify does. OWFolderWriter.Serialize wrote 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.Write settles 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. WriteElement still never renames an element already in the folder.
  • ChangesAsync pages the feed. It sent its cursor as cursor=, and the changes feed takes since, refusing any
    other parameter with a 422. So OWSync.IncrementalAsync failed whenever a world had changed since its baseline. It
    now sends since.
  • A wire check against the API's own document. OWWireContractTests records every request OWClient can 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.py refreshes the snapshot (--check says whether the server's document has
    moved).
  • EditLinksAsync reaches 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 posts add and remove to /{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 in testables and references it by file: from outside its own folder runs
    all three instead of skipping them.
  • CreateAsync sends Idempotency-Key only 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 409 id_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 idempotencyKey and no id, CreateAsync and BulkAsync minted a fresh id on
    every call, so a retry sent a different body and the server answered 409 idempotency_error instead 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.IncrementalAsync resumes from the server's own cursor, kept as is in the cache's new CursorToken,
    where it built one from the last change_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 its change_seq, as before. A 409 resync_required now rebuilds the cache
    from the start, with a RebaselineReason saying 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 as OWWorldCache.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
    HasChangesAsync says 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 its change_seq and the last cursor the server gave, where it recorded the highest change_seq it
    had read; a bulk import gives many changes one change_seq, so that number could claim changes never read. An
    empty world's cache no longer rebuilds on every sync.
  • ChangesAsync sends limit: the client's PageSize, at most 1000, above which the feed answers 422. The
    server's default page was 500; the npm SDK asks for 100.
  • OWApiError.IsRetryable reads the error code, where it was any 429 or 5xx. A 409 idempotency_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 503 ask_unavailable or media_unavailable
    (a feature switched off) no longer do.
  • OWApiError.IsAuthError reads the error code, as the npm SDK does: invalid_credentials, key_revoked (and
    the reserved world_gone), where it was any 401 or 403. A 403 such as not_author or owner_only comes 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. IsNotFound is a 404
    with the code not_found, so a proxy's 404 is not mistaken for a missing element. New: IsNotAuthor,
    IsOwnerOnly, and IsResyncRequired.
  • OWKey.CanWrite(OWKeyKind.Account) is false, as the npm SDK's kindCanWrite says: an account key is for the
    server's /account routes, where it mints world keys, and writes no world itself.
  • OWBulkSlot.Succeeded documents the rolled-back batch: after an atomic: true request with Errors true,
    nothing was written, yet the slots that would have succeeded still say 201 with the ids they would have had. Do not
    record those ids as created.