(function () { 'use strict'; const root = window.PixelIslandModules ||= {}; const FORMAT = 'pixel-island-phase2-compact-v1'; const SNAPSHOT_FORMAT = 'pixel-island-phase2-snapshot-v1'; const ASSET_BUNDLE_FORMAT = 'pixel-island-phase2-asset-bundle-v1'; const EVENT_LOG_LIMIT = 300; const DB_NAME = 'pixel-island-phase2-cache'; const DB_VERSION = 1; const ASSET_STORE = 'assets'; const SNAPSHOT_STORE = 'snapshots'; function isCompactState(value) { return Boolean(value && value.format === FORMAT && Array.isArray(value.assets)); } function isAssetBundle(value) { return Boolean(value && value.format === ASSET_BUNDLE_FORMAT && Array.isArray(value.assets)); } 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 normalizeEncodedPlane(value, size, emptyChar = '.') { const total = Math.max(1, Number(size) || 1) ** 2; const source = typeof value === 'string' ? value : Array.isArray(value) ? value.map((v) => v || emptyChar).join('') : ''; return (source + emptyChar.repeat(total)).slice(0, total); } function cropPlane(encoded, size, emptyChar = '.') { const text = normalizeEncodedPlane(encoded, size, emptyChar); let minX = size; let minY = size; let maxX = -1; let maxY = -1; for (let y = 0; y < size; y++) { for (let x = 0; x < size; x++) { if (text[y * size + 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 * size + minX, y * size + minX + w); } const rle = rleEncode(cropped); return rle.length < cropped.length ? { b: [minX, minY, w, h], e: 'rle', v: rle } : { b: [minX, minY, w, h], e: 'raw', v: cropped }; } function expandPlane(packed, size, emptyChar = '.') { const total = Math.max(1, Number(size) || 1) ** 2; const out = Array(total).fill(emptyChar); if (!packed || !packed.b) return out.join(''); const [x0, y0, w, h] = packed.b.map((v) => Math.max(0, Number(v) || 0)); const value = packed.e === 'rle' ? rleDecode(packed.v) : 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 < size && dy < size && src < value.length) { out[dy * size + dx] = value[src] || emptyChar; } } } return out.join(''); } function packAsset(asset) { const size = Math.max(1, Number(asset.size) || 16); const category = asset.category === 'dynamic' ? 'dynamic' : 'static'; const right = normalizeEncodedPlane(asset.faces?.right || asset.pixels || '', size, '.'); const depth = asset.meta?.depthPixels ? normalizeEncodedPlane(asset.meta.depthPixels, size, '.') : ''; 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 meta = {}; if (depth && /[1\-]/.test(depth)) meta.d = cropPlane(depth, size, '.'); if (lights.length) meta.l = lights; 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]; return { id: asset.id, h: asset.contentHash || asset.hash || null, n: asset.name || 'Untitled', c: category, t: asset.subtype || (category === 'dynamic' ? 'human' : 'other'), s: size, p: cropPlane(right, size, '.'), 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', m: Object.keys(meta).length ? meta : null }; } function unpackAsset(packed) { if (!packed || !packed.id) return null; const size = Math.max(1, Number(packed.s || packed.size) || 16); const category = packed.c === 'dynamic' || packed.category === 'dynamic' ? 'dynamic' : 'static'; const pixels = expandPlane(packed.p, size, '.'); 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 meta = { hasLight: lightPixels.length > 0, lightPixels, lightColor: lightPixels.length > 0 ? (metaPacked.lc || lightPixels[0]?.c || null) : null, depthPixels: metaPacked.d ? expandPlane(metaPacked.d, size, '.') : 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, 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', meta, contentHash: packed.h || null }; } function packPlacement(item) { return [item.id, item.assetId, Number(item.x) || 0, Number(item.y) || 0, item.placedAt || Date.now(), Number(item.version) || 1]; } function unpackPlacement(row) { if (!Array.isArray(row)) return row; return { id: row[0], assetId: row[1], x: row[2], y: row[3], placedAt: row[4], version: row[5] || 1 }; } function packDynamic(item) { return [item.id, item.assetId, Number(item.homeX) || 0, Number(item.homeY) || 0, item.createdAt || Date.now(), Number(item.version) || 1]; } function unpackDynamic(row) { if (!Array.isArray(row)) return row; return { id: row[0], assetId: row[1], homeX: row[2], homeY: row[3], createdAt: row[4], version: row[5] || 1 }; } function compactState(state) { return { schema: 3, format: FORMAT, authorName: state.authorName || 'Local Artist', assets: Array.isArray(state.assets) ? state.assets.map(packAsset) : [], placed: Array.isArray(state.placed) ? state.placed.map(packPlacement) : [], dynamicSummons: Array.isArray(state.dynamicSummons) ? state.dynamicSummons.map(packDynamic) : [], objectVotes: state.objectVotes || {}, assetVotes: state.assetVotes || {}, hiddenAssets: state.hiddenAssets || {}, hiddenObjects: state.hiddenObjects || {}, moderationReports: Array.isArray(state.moderationReports) ? state.moderationReports : [], guardrails: state.guardrails || null, settings: state.settings || null, eventLog: Array.isArray(state.eventLog) ? state.eventLog.slice(-EVENT_LOG_LIMIT) : [], sync: state.sync || { lastEventId: null } }; } function expandState(input) { if (!isCompactState(input)) return input; return { schema: 3, authorName: input.authorName || 'Local Artist', assets: input.assets.map(unpackAsset).filter(Boolean), placed: (input.placed || []).map(unpackPlacement), dynamicSummons: (input.dynamicSummons || []).map(unpackDynamic), objectVotes: input.objectVotes || {}, assetVotes: input.assetVotes || {}, hiddenAssets: input.hiddenAssets || {}, hiddenObjects: input.hiddenObjects || {}, moderationReports: Array.isArray(input.moderationReports) ? input.moderationReports : [], guardrails: input.guardrails || null, settings: input.settings || null, eventLog: Array.isArray(input.eventLog) ? input.eventLog.slice(-EVENT_LOG_LIMIT) : [], sync: input.sync || { lastEventId: null } }; } function compactSizeReport(state) { const full = JSON.stringify({ ...state, schema: 3 }); const compact = JSON.stringify(compactState(state)); return { fullBytes: full.length, compactBytes: compact.length, savedBytes: Math.max(0, full.length - compact.length), savedPercent: full.length ? Math.round((1 - compact.length / full.length) * 1000) / 10 : 0, assets: state.assets?.length || 0, objects: (state.placed?.length || 0) + (state.dynamicSummons?.length || 0) }; } function assetManifest(state) { return (state.assets || []).map((asset) => ({ id: asset.id, h: asset.contentHash || null, s: asset.size, c: asset.category, t: asset.subtype })); } function makeSnapshot(state, worldId = 'local-main') { return { schema: 3, 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)).map(packAsset); return { schema: 3, format: ASSET_BUNDLE_FORMAT, createdAt: Date.now(), assets }; } function unpackAssetBundle(bundle) { if (!isAssetBundle(bundle)) return []; return bundle.assets.map(unpackAsset).filter(Boolean); } 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 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 applyEvent(state, event) { if (!state || !event) return state; if (event.type === 'asset.upsert' && event.asset) { const asset = unpackAsset(event.asset); 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 === '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; if (!db.objectStoreNames.contains(ASSET_STORE)) db.createObjectStore(ASSET_STORE, { keyPath: 'id' }); if (!db.objectStoreNames.contains(SNAPSHOT_STORE)) db.createObjectStore(SNAPSHOT_STORE, { keyPath: 'worldId' }); }; 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'); for (const asset of assets) tx.objectStore(ASSET_STORE).put(packAsset(asset)); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); db.close(); } async function readCachedAssets(assetIds) { const ids = Array.from(assetIds || []); if (!ids.length) return []; const db = await openDb(); const rows = 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); }))); db.close(); return rows.filter(Boolean).map(unpackAsset).filter(Boolean); } 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(); } root.Phase2Sync = { FORMAT, SNAPSHOT_FORMAT, ASSET_BUNDLE_FORMAT, EVENT_LOG_LIMIT, isCompactState, isAssetBundle, rleEncode, rleDecode, cropPlane, expandPlane, packAsset, unpackAsset, compactState, expandState, compactSizeReport, assetManifest, makeSnapshot, makeAssetBundle, unpackAssetBundle, findMissingAssetIds, makeEvent, createAssetUpsertEvent, createObjectUpsertEvent, createObjectDeleteEvent, applyEvent, cacheAssets, readCachedAssets, cacheSnapshot }; })();