Something went wrong. Try again.
A Social Deduction / Deception Game
Something went wrong. Try again.
5.8 kB · 148 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149// ---------------------------------------------------------------------------// Map import — resolve a `world.tilemap.map` AT-URI into a render-ready// `MapExport` (+ atlas URL) for the engine's `buildScene`.//// This is the atproto adapter that the pure `render/` engine deliberately does// NOT contain: it knows about DIDs, PDS resolution, XRPC and blobs, so the// engine itself stays dependency-free (pixi.js only) and portable. It is plain// `fetch` + JSON — no `@atproto/api`, no pixi, no DOM beyond `fetch` — so it runs// in both the browser and node (e.g. the `scripts/smoke-map.ts` check).//// How a map record carries its render (see tilemap.world `lib/mapRecord.ts`):// the editor bakes the live scene into a self-contained `MapExport`, then stores// it as two blobs on the record's `render` field — `data` (the MapExport JSON,// minus the inline atlas) and `image` (the atlas PNG). We fetch `data`, splice// the atlas blob URL back into `sheet.dataUrl`, and hand the result to the// engine. The editable `nodes`/`grid` on the record are ignored here — the baked// render is the runtime source of truth.// ---------------------------------------------------------------------------
import type { MapExport } from '../../render/contract'
const PLC_DIRECTORY = 'https://plc.directory'
/** A parsed `at://<did>/<collection>/<rkey>` URI. */export interface AtUriParts { did: string collection: string rkey: string}
export function parseAtUri(uri: string): AtUriParts { const m = /^at:\/\/([^/]+)\/([^/]+)\/([^/]+)$/.exec(uri) if (!m) throw new Error(`not a record AT-URI: ${uri}`) return { did: m[1]!, collection: m[2]!, rkey: m[3]! }}
/** Resolve a DID to its PDS service endpoint (supports did:plc and did:web). */export async function resolvePds(did: string): Promise<string> { const doc = await fetchDidDoc(did) const svc = (doc.service ?? []).find( (s) => s.id === '#atproto_pds' || s.type === 'AtprotoPersonalDataServer', ) if (!svc?.serviceEndpoint) throw new Error(`no #atproto_pds endpoint in DID doc for ${did}`) return svc.serviceEndpoint.replace(/\/$/, '')}
interface DidService { id: string type: string serviceEndpoint: string}interface DidDoc { service?: DidService[]}
async function fetchDidDoc(did: string): Promise<DidDoc> { let url: string if (did.startsWith('did:plc:')) { url = `${PLC_DIRECTORY}/${did}` } else if (did.startsWith('did:web:')) { // did:web:host(:path…) → https://host/(path/)…/did.json const rest = did.slice('did:web:'.length).split(':').map(decodeURIComponent) const host = rest.shift()! url = `https://${host}/${rest.length ? rest.join('/') + '/' : '.well-known/'}did.json` } else { throw new Error(`unsupported DID method: ${did}`) } const res = await fetch(url) if (!res.ok) throw new Error(`DID resolution failed (${res.status}) for ${did}`) return (await res.json()) as DidDoc}
/** A blob reference as it appears in a getRecord JSON response. */interface BlobRef { $type: 'blob' ref: { $link: string } mimeType: string size: number}
/** The `render` field baked onto a `world.tilemap.map` record. */interface BakedRender { version: number cell: { w: number; h: number } bounds?: { x: number; y: number; w: number; h: number } /** the MapExport JSON (without the inline atlas dataUrl). */ data: BlobRef /** the atlas PNG (absent for a colour-only map with no art to bake). */ image?: BlobRef}
interface MapRecordValue { render?: BakedRender [k: string]: unknown}
/** The XRPC URL that streams a repo blob by CID. */function blobUrl(pds: string, did: string, cid: string): string { return `${pds}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(did)}&cid=${encodeURIComponent(cid)}`}
async function getRecord(pds: string, parts: AtUriParts): Promise<MapRecordValue> { const url = `${pds}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(parts.did)}` + `&collection=${encodeURIComponent(parts.collection)}&rkey=${encodeURIComponent(parts.rkey)}` const res = await fetch(url) if (!res.ok) throw new Error(`getRecord failed (${res.status}) for ${parts.rkey}`) const json = (await res.json()) as { value: MapRecordValue } return json.value}
export interface LoadedMap { /** render-ready scene; pass to the engine's `buildScene`. */ exp: MapExport /** URL of the baked atlas PNG; pass to the engine's `loadTexture`. null = no * art baked (colour-only map), in which case `buildScene` is called with a * null atlas and only background/areas/markers render. */ atlasUrl: string | null /** the raw record value (its `nodes`/`grid` drive server-side nav). */ record: MapRecordValue}
/** * Fetch a `world.tilemap.map` record by AT-URI and reconstruct the baked * `MapExport` plus its atlas URL. Throws if the record carries no baked render. */export async function loadMapExport(atUri: string): Promise<LoadedMap> { const parts = parseAtUri(atUri) const pds = await resolvePds(parts.did) const record = await getRecord(pds, parts)
const render = record.render if (!render?.data) { throw new Error(`map ${atUri} has no baked render — re-save it in the editor to bake one`) }
const exp = (await fetch(blobUrl(pds, parts.did, render.data.ref.$link)).then((r) => { if (!r.ok) throw new Error(`render data blob fetch failed (${r.status})`) return r.json() })) as MapExport
const atlasUrl = render.image ? blobUrl(pds, parts.did, render.image.ref.$link) : null // splice the atlas URL back into the sheet so the MapExport is self-describing // (the engine also accepts the atlas as a separate Texture arg). exp.sheet = { ...exp.sheet, dataUrl: atlasUrl }
return { exp, atlasUrl, record }}