(function () { 'use strict'; const root = window.PixelIslandModules ||= {}; const SNAPSHOT_FORMAT = 'pixel-island-phase2-snapshot-v1'; const ASSET_BUNDLE_FORMAT = 'pixel-island-phase2-asset-bundle-v2'; const ASSET_BUNDLE_FORMAT_V1 = 'pixel-island-phase2-asset-bundle-v1'; const DB_NAME = 'pixel-island-phase2-cache'; const DB_VERSION = 6; const ASSET_STORE = 'assets'; const SNAPSHOT_STORE = 'snapshots'; const PIXEL_BLOB_STORE = 'pixelBlobs'; const OUTBOX_STORE = 'outbox'; const COLOR_CODES = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; const PACK6_CHARS = `.${COLOR_CODES}`; function isAssetBundle(value) { return Boolean(value && (value.format === ASSET_BUNDLE_FORMAT || value.format === ASSET_BUNDLE_FORMAT_V1) && Array.isArray(value.assets)); } function bytesToBase64(bytes) { if (!bytes || !bytes.length) return ''; if (typeof btoa === 'function') { let binary = ''; const chunk = 8192; for (let i = 0; i < bytes.length; i += chunk) { binary += String.fromCharCode(...bytes.slice(i, i + chunk)); } return btoa(binary); } if (typeof Buffer !== 'undefined') return Buffer.from(bytes).toString('base64'); return ''; } function base64ToBytes(value) { const text = String(value || ''); if (!text) return []; if (typeof atob === 'function') { const binary = atob(text); const out = new Array(binary.length); for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i) & 255; return out; } if (typeof Buffer !== 'undefined') return Array.from(Buffer.from(text, 'base64')); return []; } function pack6Index(ch) { const index = PACK6_CHARS.indexOf(ch || '.'); return index >= 0 && index < 64 ? index : 0; } function unpack6Index(value) { return PACK6_CHARS[value] || '.'; } function canPack6(text) { for (const ch of String(text || '')) if (PACK6_CHARS.indexOf(ch) < 0) return false; return true; } function pack6Text(input) { const text = String(input || ''); if (!text) return ''; let buffer = 0; let bitCount = 0; const bytes = []; for (const ch of text) { buffer = (buffer << 6) | pack6Index(ch); bitCount += 6; while (bitCount >= 8) { bitCount -= 8; bytes.push((buffer >> bitCount) & 255); buffer &= (1 << bitCount) - 1; } } if (bitCount > 0) bytes.push((buffer << (8 - bitCount)) & 255); return bytesToBase64(bytes); } function unpack6Text(input, length) { const target = Math.max(0, Number(length) || 0); if (!target) return ''; const bytes = base64ToBytes(input); let buffer = 0; let bitCount = 0; let out = ''; for (const byte of bytes) { buffer = (buffer << 8) | (byte & 255); bitCount += 8; while (bitCount >= 6 && out.length < target) { bitCount -= 6; out += unpack6Index((buffer >> bitCount) & 63); buffer &= (1 << bitCount) - 1; } if (out.length >= target) break; } return (out + '.'.repeat(target)).slice(0, target); } function packBitMask(mask) { const bits = Array.from(mask || [], Boolean); if (!bits.length) return ''; const bytes = []; let current = 0; for (let i = 0; i < bits.length; i++) { current = (current << 1) | (bits[i] ? 1 : 0); if (i % 8 === 7) { bytes.push(current & 255); current = 0; } } const rest = bits.length % 8; if (rest) bytes.push((current << (8 - rest)) & 255); return bytesToBase64(bytes); } function unpackBitMask(input, length) { const target = Math.max(0, Number(length) || 0); const bytes = base64ToBytes(input); const out = []; for (const byte of bytes) { for (let bit = 7; bit >= 0 && out.length < target; bit--) out.push(Boolean((byte >> bit) & 1)); if (out.length >= target) break; } while (out.length < target) out.push(false); return out; } function rleEncode(input) { const text = String(input || ''); if (!text) return ''; let out = ''; let last = text[0]; let count = 1; for (let i = 1; i < text.length; i++) { const ch = text[i]; if (ch === last) count++; else { out += `${count}:${last}`; last = ch; count = 1; } } out += `${count}:${last}`; return out; } function rleDecode(input) { const text = String(input || ''); if (!text) return ''; let out = ''; let i = 0; while (i < text.length) { let digits = ''; while (i < text.length && text[i] >= '0' && text[i] <= '9') digits += text[i++]; if (text[i] !== ':') break; i++; const ch = text[i++] || ''; const count = Math.max(0, Number(digits) || 0); out += ch.repeat(count); } return out; } function assetWidth(asset) { const size = Math.max(1, Number(asset?.size) || 16); return Math.max(1, Number(asset?.width ?? asset?.w ?? size) || size); } function assetHeight(asset) { const size = Math.max(1, Number(asset?.size) || 16); return Math.max(1, Number(asset?.height ?? asset?.ht ?? size) || size); } function normalizeEncodedPlane(value, width, emptyChar = '.', height = width) { const w = Math.max(1, Number(width) || 1); const h = Math.max(1, Number(height) || w); const total = w * h; const source = typeof value === 'string' ? value : Array.isArray(value) ? value.map((v) => v || emptyChar).join('') : ''; return (source + emptyChar.repeat(total)).slice(0, total); } function chooseSmallest(candidates) { let best = candidates[0]; let bestSize = JSON.stringify(best).length; for (const candidate of candidates.slice(1)) { const size = JSON.stringify(candidate).length; if (size < bestSize) { best = candidate; bestSize = size; } } return best; } function cropPlane(encoded, width, emptyChar = '.', options = {}, height = width) { const w0 = Math.max(1, Number(width) || 1); const h0 = Math.max(1, Number(height) || w0); const text = normalizeEncodedPlane(encoded, w0, emptyChar, h0); let minX = w0; let minY = h0; let maxX = -1; let maxY = -1; for (let y = 0; y < h0; y++) { for (let x = 0; x < w0; x++) { if (text[y * w0 + x] !== emptyChar) { minX = Math.min(minX, x); minY = Math.min(minY, y); maxX = Math.max(maxX, x); maxY = Math.max(maxY, y); } } } if (maxX < 0) return { b: null, e: 'raw', v: '' }; const w = maxX - minX + 1; const h = maxY - minY + 1; let cropped = ''; for (let y = minY; y <= maxY; y++) cropped += text.slice(y * w0 + minX, y * w0 + minX + w); const candidates = [ { b: [minX, minY, w, h], e: 'raw', v: cropped }, { b: [minX, minY, w, h], e: 'rle', v: rleEncode(cropped) } ]; if (options.bitPack && canPack6(cropped)) candidates.push({ b: [minX, minY, w, h], e: 'bp6', v: pack6Text(cropped), n: cropped.length }); return chooseSmallest(candidates); } function expandPlane(packed, width, emptyChar = '.', baseEncoded = null, height = width) { const w0 = Math.max(1, Number(width) || 1); const h0 = Math.max(1, Number(height) || w0); const total = w0 * h0; const out = Array(total).fill(emptyChar); if (baseEncoded) { const base = normalizeEncodedPlane(baseEncoded, w0, emptyChar, h0); for (let i = 0; i < Math.min(out.length, base.length); i++) out[i] = base[i] || emptyChar; } if (!packed || !packed.b) return out.join(''); const [x0, y0, w, h] = packed.b.map((v) => Math.max(0, Number(v) || 0)); let value = ''; if (packed.e === 'rle') value = rleDecode(packed.v); else if (packed.e === 'bp6') value = unpack6Text(packed.v, packed.n || (w * h)); else value = String(packed.v || ''); for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { const src = y * w + x; const dx = x0 + x; const dy = y0 + y; if (dx >= 0 && dy >= 0 && dx < w0 && dy < h0 && src < value.length) out[dy * w0 + dx] = value[src] || emptyChar; } } return out.join(''); } function planeForAsset(asset) { const w = assetWidth(asset); const h = assetHeight(asset); return normalizeEncodedPlane(asset?.faces?.right || asset?.pixels || '', w, '.', h); } function selectPixelPlanePack(asset, assetMap) { const w = assetWidth(asset); const h = assetHeight(asset); const right = planeForAsset(asset); return cropPlane(right, w, '.', { bitPack: true }, h); } function packAsset(asset, assetMap = null) { const width = assetWidth(asset); const height = assetHeight(asset); const size = Math.max(width, height, Math.max(1, Number(asset.size) || 16)); const category = asset.category === 'dynamic' ? 'dynamic' : 'static'; const depth = asset.meta?.depthPixels ? normalizeEncodedPlane(asset.meta.depthPixels, width, '.', height) : ''; const lights = Array.isArray(asset.meta?.lightPixels) ? asset.meta.lightPixels.map((p) => [Number(p.x) || 0, Number(p.y) || 0, p.c || asset.meta?.lightColor || '']).filter((p) => p[2]) : []; const particles = Array.isArray(asset.meta?.particlePixels) ? asset.meta.particlePixels.map((p) => [Number(p.x) || 0, Number(p.y) || 0, p.c || '', p.dir || 'up']).filter((p) => p[2]) : []; const meta = {}; if (depth && /[1\-]/.test(depth)) meta.d = cropPlane(depth, width, '.', {}, height); if (lights.length) meta.l = lights; if (particles.length) meta.pt = particles; if (asset.meta?.lightColor) meta.lc = asset.meta.lightColor; if (asset.meta?.door) meta.dr = [Number(asset.meta.door.x) || 0, Number(asset.meta.door.y) || 0]; if (asset.meta?.particleConfig?.enabled && !particles.length) { const pc = asset.meta.particleConfig; meta.pc = [pc.c || '', pc.dir || 'up', null]; } return { id: asset.id, h: asset.contentHash || asset.hash || null, bi: asset.blobId || asset.bi || null, n: asset.name || 'Untitled', c: category, t: asset.subtype || (category === 'dynamic' ? 'human' : 'other'), s: size, w: width !== size ? width : undefined, ht: height !== size ? height : undefined, p: selectPixelPlanePack(asset, assetMap), f: category === 'dynamic' ? { l: 'mirror' } : null, pa: asset.parentAssetId || null, oa: asset.originalAssetId || null, ca: asset.createdAt || Date.now(), ua: asset.updatedAt || asset.createdAt || Date.now(), au: asset.author || 'Local Artist', ow: asset.ownerAccountId || null, v: Number(asset.version) || 1, m: Object.keys(meta).length ? meta : null }; } function packAssetMetadata(asset, assetMap = null) { const packed = packAsset(asset, assetMap); delete packed.p; return packed; } function packPixelBlob(input) { if (!input) return null; const width = assetWidth(input); const height = assetHeight(input); const payload = normalizeEncodedPlane(input.payload || input.faces?.right || input.pixels || '', width, '.', height); const id = input.blobId || input.bi || ((input.codec || input.payload) ? input.id : null); if (!id) return null; return { id, co: input.codec || 'palette-index-v1', w: width, ht: height, p: cropPlane(payload, width, '.', { bitPack: true }, height), ca: input.createdAt || Date.now(), ua: input.updatedAt || input.createdAt || Date.now() }; } function unpackPixelBlob(packed) { if (!packed || !packed.id) return null; const width = Math.max(1, Number(packed.w || packed.width) || 16); const height = Math.max(1, Number(packed.ht || packed.height || width) || width); return { id: packed.id, codec: packed.co || packed.codec || 'palette-index-v1', width, height, payload: expandPlane(packed.p, width, '.', null, height), createdAt: packed.ca || null, updatedAt: packed.ua || packed.ca || null }; } function unpackAsset(packed, assetById = null, pixelBlobById = null) { if (!packed || !packed.id) return null; const size = Math.max(1, Number(packed.s || packed.size) || 16); const width = Math.max(1, Number(packed.w || packed.width || size) || size); const height = Math.max(1, Number(packed.ht || packed.height || size) || size); const category = packed.c === 'dynamic' || packed.category === 'dynamic' ? 'dynamic' : 'static'; const blobId = packed.bi || packed.blobId || null; const pixelBlob = blobId && pixelBlobById ? (typeof pixelBlobById.get === 'function' ? pixelBlobById.get(blobId) : pixelBlobById[blobId]) : null; const blobPayload = typeof pixelBlob?.payload === 'string' ? pixelBlob.payload : null; const pixels = blobPayload ? normalizeEncodedPlane(blobPayload, width, '.', height) : expandPlane(packed.p, width, '.', null, height); if (pixels == null) return null; const metaPacked = packed.m || {}; const lightPixels = Array.isArray(metaPacked.l) ? metaPacked.l.map((p) => ({ x: Number(p[0]) || 0, y: Number(p[1]) || 0, c: p[2] || metaPacked.lc || 'a' })) : []; const particlePixels = Array.isArray(metaPacked.pt) ? metaPacked.pt.map((p) => ({ x: Number(p[0]) || 0, y: Number(p[1]) || 0, c: p[2] || 'a', dir: p[3] || 'up' })) : []; const particleConfig = Array.isArray(metaPacked.pc) ? { enabled: true, c: metaPacked.pc[0] || 'a', dir: metaPacked.pc[1] || 'up', range: Array.isArray(metaPacked.pc[2]) ? { x: metaPacked.pc[2][0] || 0, y: metaPacked.pc[2][1] || 0, w: metaPacked.pc[2][2] || 0, h: metaPacked.pc[2][3] || 0 } : null } : null; const meta = { hasLight: lightPixels.length > 0, lightPixels, lightColor: lightPixels.length > 0 ? (metaPacked.lc || lightPixels[0]?.c || null) : null, hasParticles: Boolean(particleConfig) || particlePixels.length > 0, particlePixels, particleConfig, depthPixels: metaPacked.d ? expandPlane(metaPacked.d, width, '.', null, height) : null, door: Array.isArray(metaPacked.dr) ? { x: Number(metaPacked.dr[0]) || 0, y: Number(metaPacked.dr[1]) || 0 } : null }; return { id: packed.id, name: packed.n || 'Untitled', category, subtype: packed.t || (category === 'dynamic' ? 'human' : 'other'), size, width, height, blobId, pixels, faces: category === 'dynamic' ? { right: pixels, left: 'mirror' } : null, parentAssetId: packed.pa || null, originalAssetId: packed.oa || null, createdAt: packed.ca || Date.now(), updatedAt: packed.ua || packed.ca || Date.now(), author: packed.au || 'Local Artist', ownerAccountId: packed.ow || packed.ownerAccountId || '', version: Number(packed.v || packed.version) || 1, meta, contentHash: packed.h || null }; } function unpackAssets(rows, pixelBlobById = null) { return (Array.isArray(rows) ? rows : []).map((row) => unpackAsset(row, null, pixelBlobById)).filter(Boolean); } function packPlacement(item) { const meta = {}; if (item.publishedAt) meta.pu = item.publishedAt; if (item.status && item.status !== 'active') meta.st = item.status; if (item.ownerAccountId) meta.ow = item.ownerAccountId; return [item.id, item.assetId, Number(item.x) || 0, Number(item.y) || 0, item.placedAt || Date.now(), Number(item.version) || 1, Object.keys(meta).length ? meta : null]; } function unpackPlacement(row) { if (!Array.isArray(row)) return row; const meta = row[6] || {}; return { id: row[0], assetId: row[1], x: row[2], y: row[3], placedAt: row[4], version: row[5] || 1, publishedAt: meta.pu || row[4], status: meta.st || 'active', ownerAccountId: meta.ow || '' }; } function packDynamic(item) { const meta = {}; if (item.publishedAt) meta.pu = item.publishedAt; if (item.status && item.status !== 'active') meta.st = item.status; if (item.ownerAccountId) meta.ow = item.ownerAccountId; return [item.id, item.assetId, Number(item.homeX) || 0, Number(item.homeY) || 0, item.createdAt || Date.now(), Number(item.version) || 1, Object.keys(meta).length ? meta : null]; } function unpackDynamic(row) { if (!Array.isArray(row)) return row; const meta = row[6] || {}; return { id: row[0], assetId: row[1], homeX: row[2], homeY: row[3], createdAt: row[4], version: row[5] || 1, publishedAt: meta.pu || row[4], status: meta.st || 'active', ownerAccountId: meta.ow || '' }; } function assetMapFor(assets) { return new Map((assets || []).map((asset) => [asset.id, asset])); } function assetManifest(state) { return (state.assets || []).map((asset) => ({ id: asset.id, h: asset.contentHash || null, bi: asset.blobId || null, s: asset.size, w: asset.width || null, ht: asset.height || null, c: asset.category, t: asset.subtype, pa: asset.parentAssetId || null, oa: asset.originalAssetId || null })); } function makeSnapshot(state, worldId = 'local-main') { return { schema: 4, format: SNAPSHOT_FORMAT, worldId, createdAt: Date.now(), manifest: assetManifest(state), placed: (state.placed || []).map(packPlacement), dynamicSummons: (state.dynamicSummons || []).map(packDynamic), hiddenObjects: state.hiddenObjects || {} }; } function makeAssetBundle(state, assetIds) { const wanted = new Set(assetIds || []); const assets = (state.assets || []).filter((asset) => !wanted.size || wanted.has(asset.id)); const map = assetMapFor(assets); return { schema: 4, format: ASSET_BUNDLE_FORMAT, createdAt: Date.now(), assets: assets.map((asset) => packAsset(asset, map)) }; } function unpackAssetBundle(bundle) { if (!isAssetBundle(bundle)) return []; return unpackAssets(bundle.assets); } function findMissingAssetIds(snapshot, knownAssetIds) { const known = new Set(knownAssetIds || []); return (snapshot?.manifest || []).map((asset) => asset.id).filter((id) => id && !known.has(id)); } function makeEvent(type, payload = {}) { return { id: `ev_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`, type, at: Date.now(), ...payload }; } function createAssetUpsertEvent(asset) { return makeEvent('asset.upsert', { asset: packAsset(asset) }); } function createAssetDeleteEvent(assetId) { return makeEvent('asset.delete', { assetId }); } function createObjectUpsertEvent(kind, object) { return makeEvent('object.upsert', { kind, object: kind === 'dynamic' ? packDynamic(object) : packPlacement(object) }); } function createObjectDeleteEvent(kind, objectId) { return makeEvent('object.delete', { kind, objectId }); } function deleteAssetOnly(state, assetId) { if (!state || !assetId) return state; const removedObjectIds = new Set(); for (const item of state.placed || []) if (item.assetId === assetId) removedObjectIds.add(item.id); for (const item of state.dynamicSummons || []) if (item.assetId === assetId) removedObjectIds.add(item.id); state.assets = (state.assets || []).filter((asset) => asset.id !== assetId); state.placed = (state.placed || []).filter((item) => item.assetId !== assetId); state.dynamicSummons = (state.dynamicSummons || []).filter((item) => item.assetId !== assetId); delete state.assetVotes?.[assetId]; delete state.hiddenAssets?.[assetId]; for (const id of removedObjectIds) delete state.objectVotes?.[id], delete state.hiddenObjects?.[id]; state.moderationReports = (state.moderationReports || []).filter((report) => !removedObjectIds.has(report.objectId)); return state; } function applyEvent(state, event) { if (!state || !event) return state; if (event.type === 'asset.upsert' && event.asset) { const byId = assetMapFor(state.assets || []); const asset = unpackAsset(event.asset, byId); if (!asset) return state; const index = (state.assets || []).findIndex((a) => a.id === asset.id); if (index >= 0) state.assets[index] = asset; else (state.assets ||= []).unshift(asset); } if (event.type === 'asset.delete' && event.assetId) { deleteAssetOnly(state, event.assetId); } if (event.type === 'object.upsert') { if (event.kind === 'dynamic') { const object = unpackDynamic(event.object); const list = state.dynamicSummons ||= []; const index = list.findIndex((item) => item.id === object.id); if (index >= 0) list[index] = object; else list.push(object); } else { const object = unpackPlacement(event.object); const list = state.placed ||= []; const index = list.findIndex((item) => item.id === object.id); if (index >= 0) list[index] = object; else list.push(object); } } if (event.type === 'object.delete') { const key = event.kind === 'dynamic' ? 'dynamicSummons' : 'placed'; state[key] = (state[key] || []).filter((item) => item.id !== event.objectId); } return state; } function openDb() { if (!('indexedDB' in window)) return Promise.reject(new Error('IndexedDB is not available.')); return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onupgradeneeded = () => { const db = request.result; const tx = request.transaction; if (!db.objectStoreNames.contains(ASSET_STORE)) db.createObjectStore(ASSET_STORE, { keyPath: 'id' }); if (!db.objectStoreNames.contains(SNAPSHOT_STORE)) db.createObjectStore(SNAPSHOT_STORE, { keyPath: 'worldId' }); if (!db.objectStoreNames.contains(PIXEL_BLOB_STORE)) db.createObjectStore(PIXEL_BLOB_STORE, { keyPath: 'id' }); if (!db.objectStoreNames.contains(OUTBOX_STORE)) db.createObjectStore(OUTBOX_STORE, { keyPath: 'id' }); if (request.oldVersion && request.oldVersion < DB_VERSION) { if (db.objectStoreNames.contains(ASSET_STORE)) tx.objectStore(ASSET_STORE).clear(); if (db.objectStoreNames.contains(SNAPSHOT_STORE)) tx.objectStore(SNAPSHOT_STORE).clear(); if (db.objectStoreNames.contains(PIXEL_BLOB_STORE)) tx.objectStore(PIXEL_BLOB_STORE).clear(); if (db.objectStoreNames.contains(OUTBOX_STORE)) tx.objectStore(OUTBOX_STORE).clear(); } }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async function cacheAssets(assets) { if (!Array.isArray(assets) || !assets.length) return; const db = await openDb(); await new Promise((resolve, reject) => { const tx = db.transaction(ASSET_STORE, 'readwrite'); const assetStore = tx.objectStore(ASSET_STORE); const map = assetMapFor(assets); for (const asset of assets) assetStore.put(packAssetMetadata(asset, map)); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); db.close(); } async function cachePixelBlobs(pixelBlobs) { if (!Array.isArray(pixelBlobs) || !pixelBlobs.length) return; const db = await openDb(); await new Promise((resolve, reject) => { const tx = db.transaction(PIXEL_BLOB_STORE, 'readwrite'); const store = tx.objectStore(PIXEL_BLOB_STORE); for (const pixelBlob of pixelBlobs) { const packed = packPixelBlob(pixelBlob); if (packed) store.put(packed); } tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); db.close(); } async function readCachedPixelBlobs(blobIds) { const ids = Array.from(blobIds || []).filter(Boolean); if (!ids.length) return []; const db = await openDb(); const rows = await Promise.all(ids.map((id) => new Promise((resolve) => { const request = db.transaction(PIXEL_BLOB_STORE, 'readonly').objectStore(PIXEL_BLOB_STORE).get(id); request.onsuccess = () => resolve(request.result || null); request.onerror = () => resolve(null); }))); db.close(); return rows.map(unpackPixelBlob).filter(Boolean); } async function readCachedAssets(assetIds) { const ids = Array.from(assetIds || []); if (!ids.length) return []; const db = await openDb(); const assetRows = await Promise.all(ids.map((id) => new Promise((resolve) => { const request = db.transaction(ASSET_STORE, 'readonly').objectStore(ASSET_STORE).get(id); request.onsuccess = () => resolve(request.result || null); request.onerror = () => resolve(null); }))); const blobIds = assetRows.map((row) => row?.bi || row?.blobId).filter(Boolean); const blobRows = await Promise.all(blobIds.map((id) => new Promise((resolve) => { const request = db.transaction(PIXEL_BLOB_STORE, 'readonly').objectStore(PIXEL_BLOB_STORE).get(id); request.onsuccess = () => resolve(request.result || null); request.onerror = () => resolve(null); }))); db.close(); const blobById = new Map(blobRows.map(unpackPixelBlob).filter(Boolean).map((blob) => [blob.id, blob])); return unpackAssets(assetRows.filter(Boolean), blobById); } async function cacheSnapshot(snapshot) { if (!snapshot?.worldId) return; const db = await openDb(); await new Promise((resolve, reject) => { const tx = db.transaction(SNAPSHOT_STORE, 'readwrite'); tx.objectStore(SNAPSHOT_STORE).put(snapshot); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); db.close(); } async function readCachedSnapshot(worldId = 'local-main') { const db = await openDb(); const row = await new Promise((resolve) => { const request = db.transaction(SNAPSHOT_STORE, 'readonly').objectStore(SNAPSHOT_STORE).get(worldId); request.onsuccess = () => resolve(request.result || null); request.onerror = () => resolve(null); }); db.close(); if (!row) return null; return { ...row, placed: Array.isArray(row.placed) ? row.placed.map(unpackPlacement) : [], dynamicSummons: Array.isArray(row.dynamicSummons) ? row.dynamicSummons.map(unpackDynamic) : [], hiddenObjects: row.hiddenObjects || {} }; } async function deleteCachedAssets(assetIds, blobIds = []) { const assets = Array.from(assetIds || []).filter(Boolean); const blobs = Array.from(blobIds || []).filter(Boolean); if (!assets.length && !blobs.length) return; const db = await openDb(); await new Promise((resolve, reject) => { const tx = db.transaction([ASSET_STORE, PIXEL_BLOB_STORE], 'readwrite'); const assetStore = tx.objectStore(ASSET_STORE); const blobStore = tx.objectStore(PIXEL_BLOB_STORE); for (const id of assets) assetStore.delete(id); for (const id of blobs) blobStore.delete(id); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); db.close(); } async function cacheOutboxCommand(command) { if (!command?.id) return; const db = await openDb(); await new Promise((resolve, reject) => { const tx = db.transaction(OUTBOX_STORE, 'readwrite'); tx.objectStore(OUTBOX_STORE).put({ ...command, queuedAt: command.queuedAt || Date.now() }); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); db.close(); } async function readOutboxCommands(limit = 300) { const db = await openDb(); const rows = await new Promise((resolve) => { const request = db.transaction(OUTBOX_STORE, 'readonly').objectStore(OUTBOX_STORE).getAll(); request.onsuccess = () => resolve(Array.isArray(request.result) ? request.result : []); request.onerror = () => resolve([]); }); db.close(); return rows .sort((a, b) => Number(a.createdAt || a.queuedAt || 0) - Number(b.createdAt || b.queuedAt || 0)) .slice(-Math.max(1, Number(limit) || 300)); } async function deleteOutboxCommands(commandIds) { const ids = Array.from(commandIds || []).filter(Boolean); if (!ids.length) return; const db = await openDb(); await new Promise((resolve, reject) => { const tx = db.transaction(OUTBOX_STORE, 'readwrite'); const store = tx.objectStore(OUTBOX_STORE); for (const id of ids) store.delete(id); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); db.close(); } async function clearCache() { const db = await openDb(); await new Promise((resolve, reject) => { const tx = db.transaction([ASSET_STORE, PIXEL_BLOB_STORE, SNAPSHOT_STORE, OUTBOX_STORE], 'readwrite'); tx.objectStore(ASSET_STORE).clear(); tx.objectStore(PIXEL_BLOB_STORE).clear(); tx.objectStore(SNAPSHOT_STORE).clear(); tx.objectStore(OUTBOX_STORE).clear(); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); db.close(); } root.Phase2Sync = { SNAPSHOT_FORMAT, ASSET_BUNDLE_FORMAT, ASSET_BUNDLE_FORMAT_V1, isAssetBundle, rleEncode, rleDecode, pack6Text, unpack6Text, packBitMask, unpackBitMask, cropPlane, expandPlane, packAsset, packAssetMetadata, packPixelBlob, unpackPixelBlob, unpackAsset, unpackAssets, packPlacement, unpackPlacement, packDynamic, unpackDynamic, assetManifest, makeSnapshot, makeAssetBundle, unpackAssetBundle, findMissingAssetIds, makeEvent, createAssetUpsertEvent, createAssetDeleteEvent, createObjectUpsertEvent, createObjectDeleteEvent, deleteAssetOnly, applyEvent, cacheAssets, cachePixelBlobs, readCachedPixelBlobs, readCachedAssets, cacheSnapshot, readCachedSnapshot, deleteCachedAssets, cacheOutboxCommand, readOutboxCommands, deleteOutboxCommands, clearCache }; })();