hmm
This commit is contained in:
parent
06ab438d7b
commit
92abfa3c89
3 changed files with 644 additions and 316 deletions
|
|
@ -2,23 +2,18 @@
|
|||
'use strict';
|
||||
|
||||
const root = window.PixelIslandModules ||= {};
|
||||
const FORMAT = 'pixel-island-phase2-compact-v2';
|
||||
const FORMAT_V1 = 'pixel-island-phase2-compact-v1';
|
||||
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 EVENT_LOG_LIMIT = 300;
|
||||
const DB_NAME = 'pixel-island-phase2-cache';
|
||||
const DB_VERSION = 1;
|
||||
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 isCompactState(value) {
|
||||
return Boolean(value && (value.format === FORMAT || value.format === FORMAT_V1) && Array.isArray(value.assets));
|
||||
}
|
||||
|
||||
function isAssetBundle(value) {
|
||||
return Boolean(value && (value.format === ASSET_BUNDLE_FORMAT || value.format === ASSET_BUNDLE_FORMAT_V1) && Array.isArray(value.assets));
|
||||
}
|
||||
|
|
@ -292,6 +287,7 @@
|
|||
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'),
|
||||
|
|
@ -311,13 +307,58 @@
|
|||
};
|
||||
}
|
||||
|
||||
function unpackAsset(packed, assetById = 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 pixels = expandPlane(packed.p, width, '.', null, height);
|
||||
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)
|
||||
|
|
@ -347,6 +388,7 @@
|
|||
size,
|
||||
width,
|
||||
height,
|
||||
blobId,
|
||||
pixels,
|
||||
faces: category === 'dynamic' ? { right: pixels, left: 'mirror' } : null,
|
||||
parentAssetId: packed.pa || null,
|
||||
|
|
@ -361,8 +403,8 @@
|
|||
};
|
||||
}
|
||||
|
||||
function unpackAssets(rows) {
|
||||
return (Array.isArray(rows) ? rows : []).map((row) => unpackAsset(row)).filter(Boolean);
|
||||
function unpackAssets(rows, pixelBlobById = null) {
|
||||
return (Array.isArray(rows) ? rows : []).map((row) => unpackAsset(row, null, pixelBlobById)).filter(Boolean);
|
||||
}
|
||||
|
||||
function packPlacement(item) {
|
||||
|
|
@ -397,75 +439,8 @@
|
|||
return new Map((assets || []).map((asset) => [asset.id, asset]));
|
||||
}
|
||||
|
||||
function compactState(state) {
|
||||
const assets = Array.isArray(state.assets) ? state.assets : [];
|
||||
const map = assetMapFor(assets);
|
||||
return {
|
||||
schema: 4,
|
||||
format: FORMAT,
|
||||
authorName: state.authorName || 'Local Artist',
|
||||
assets: assets.map((asset) => packAsset(asset, map)),
|
||||
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,
|
||||
account: state.account || null,
|
||||
publishLog: Array.isArray(state.publishLog) ? state.publishLog.slice(-300) : [],
|
||||
eventLog: Array.isArray(state.eventLog) ? state.eventLog.slice(-EVENT_LOG_LIMIT) : [],
|
||||
sync: state.sync || { lastEventId: null },
|
||||
worldMode: state.worldMode === 'shared' ? 'shared' : 'local',
|
||||
serverSync: state.serverSync || { lastServerEventId: null, pendingCommands: [] },
|
||||
tombstones: state.tombstones || { assets: {}, objects: {} },
|
||||
deletedSeedAssetNames: Array.isArray(state.deletedSeedAssetNames) ? state.deletedSeedAssetNames : []
|
||||
};
|
||||
}
|
||||
|
||||
function expandState(input) {
|
||||
if (!isCompactState(input)) return input;
|
||||
return {
|
||||
schema: input.schema || 4,
|
||||
authorName: input.authorName || 'Local Artist',
|
||||
assets: unpackAssets(input.assets),
|
||||
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,
|
||||
account: input.account || null,
|
||||
publishLog: Array.isArray(input.publishLog) ? input.publishLog.slice(-300) : [],
|
||||
eventLog: Array.isArray(input.eventLog) ? input.eventLog.slice(-EVENT_LOG_LIMIT) : [],
|
||||
sync: input.sync || { lastEventId: null },
|
||||
worldMode: input.worldMode === 'shared' ? 'shared' : 'local',
|
||||
serverSync: input.serverSync || { lastServerEventId: null, pendingCommands: [] },
|
||||
tombstones: input.tombstones || { assets: {}, objects: {} },
|
||||
deletedSeedAssetNames: Array.isArray(input.deletedSeedAssetNames) ? input.deletedSeedAssetNames : []
|
||||
};
|
||||
}
|
||||
|
||||
function compactSizeReport(state) {
|
||||
const full = JSON.stringify({ ...state, schema: 4 });
|
||||
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, w: asset.width || null, ht: asset.height || null, c: asset.category, t: asset.subtype, pa: asset.parentAssetId || null, oa: asset.originalAssetId || null }));
|
||||
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') {
|
||||
|
|
@ -574,8 +549,17 @@
|
|||
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);
|
||||
|
|
@ -587,24 +571,62 @@
|
|||
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));
|
||||
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 readCachedAssets(assetIds) {
|
||||
const ids = Array.from(assetIds || []);
|
||||
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(ASSET_STORE, 'readonly').objectStore(ASSET_STORE).get(id);
|
||||
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 unpackAssets(rows.filter(Boolean));
|
||||
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) {
|
||||
|
|
@ -619,14 +641,98 @@
|
|||
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 = {
|
||||
FORMAT,
|
||||
FORMAT_V1,
|
||||
SNAPSHOT_FORMAT,
|
||||
ASSET_BUNDLE_FORMAT,
|
||||
ASSET_BUNDLE_FORMAT_V1,
|
||||
EVENT_LOG_LIMIT,
|
||||
isCompactState,
|
||||
isAssetBundle,
|
||||
rleEncode,
|
||||
rleDecode,
|
||||
|
|
@ -637,15 +743,15 @@
|
|||
cropPlane,
|
||||
expandPlane,
|
||||
packAsset,
|
||||
packAssetMetadata,
|
||||
packPixelBlob,
|
||||
unpackPixelBlob,
|
||||
unpackAsset,
|
||||
unpackAssets,
|
||||
packPlacement,
|
||||
unpackPlacement,
|
||||
packDynamic,
|
||||
unpackDynamic,
|
||||
compactState,
|
||||
expandState,
|
||||
compactSizeReport,
|
||||
assetManifest,
|
||||
makeSnapshot,
|
||||
makeAssetBundle,
|
||||
|
|
@ -659,7 +765,15 @@
|
|||
deleteAssetOnly,
|
||||
applyEvent,
|
||||
cacheAssets,
|
||||
cachePixelBlobs,
|
||||
readCachedPixelBlobs,
|
||||
readCachedAssets,
|
||||
cacheSnapshot
|
||||
cacheSnapshot,
|
||||
readCachedSnapshot,
|
||||
deleteCachedAssets,
|
||||
cacheOutboxCommand,
|
||||
readOutboxCommands,
|
||||
deleteOutboxCommands,
|
||||
clearCache
|
||||
};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -2838,3 +2838,9 @@ body, button, input, select, textarea { font-size: 15px; }
|
|||
.collectionSearch { flex-basis:104px !important; width:104px !important; }
|
||||
.studioDrawer { z-index: var(--drawer-z) !important; }
|
||||
}
|
||||
|
||||
/* Keep Collection list redraws from letting scroll anchoring snap the drawer to the top. */
|
||||
.drawerBody,
|
||||
.assetList {
|
||||
overflow-anchor: none;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue