From c6afbf19ee11ee19bec2812eba878a9e73f980b1 Mon Sep 17 00:00:00 2001 From: L3P3 Date: Thu, 21 Aug 2025 14:39:47 +0200 Subject: [PATCH 1/8] optimizations, wait for db on start --- package.json | 2 +- src/app.css | 4 +- src/app.js | 15 +- src/etc/helpers.js | 3 +- src/etc/storage.js | 353 +++++++++++++++++++++----------------------- src/game/m_game.js | 4 +- src/game/m_world.js | 6 +- src/os/c_window.js | 153 ++++++++----------- 8 files changed, 251 insertions(+), 289 deletions(-) diff --git a/package.json b/package.json index d7e5c4c..66def5e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minicraft", - "version": "0.11.7", + "version": "0.11.8", "description": "voxel-based 3d game, written in javascript", "homepage": "https://l3p3.de/minicraft", "repository": { diff --git a/src/app.css b/src/app.css index daa6c1b..96198b5 100644 --- a/src/app.css +++ b/src/app.css @@ -403,10 +403,10 @@ button { width: 1rem; height: 1rem; border-radius: 50%; - opacity: 50%; + opacity: .5; cursor: pointer; transition: opacity .2s; } .window_button:hover { - opacity: 100%; + opacity: 1; } diff --git a/src/app.js b/src/app.js index feaa8b7..bbb7fc0 100755 --- a/src/app.js +++ b/src/app.js @@ -38,6 +38,9 @@ import { app_state, hook_app_state, } from './etc/state.js'; +import { + chunks_db_promise, +} from './etc/storage.js'; import { game_save, @@ -187,7 +190,11 @@ if (window.SSR) { } else if (BroadcastChannel_) { const channel_lock = new BroadcastChannel_('minicraft.lock'); - const timeout = setTimeout_(init, 100, Root); + const timeout = setTimeout_(() => ( + chunks_db_promise.then(() => + init(Root) + ) + ), 100); channel_lock.addEventListener('message', event => { if (event.data === 'yes') { clearTimeout_(timeout); @@ -206,7 +213,11 @@ else { const lock_found = Number_(localStorage_getItem('minicraft.lock')); const lock_limit = Date_now() - 1000; // if already expired - if (lock_found < lock_limit) init(Root); + if (lock_found < lock_limit) { + chunks_db_promise.then(() => + init(Root) + ) + } // if not, wait and check again else setTimeout_(() => { init( diff --git a/src/etc/helpers.js b/src/etc/helpers.js index a2ef3ad..99ac615 100644 --- a/src/etc/helpers.js +++ b/src/etc/helpers.js @@ -26,13 +26,14 @@ const Date_ = Date; export const Date_now = () => Date_.now(); const JSON_ = JSON; export const Promise_ = Promise; +export const Promise_resolve = Promise_.resolve; export const JSON_stringify = JSON_.stringify; export const JSON_parse = JSON_.parse; export const localStorage_ = localStorage; export const localStorage_getItem = key => localStorage_.getItem(key); export const localStorage_setItem = localStorage_.setItem.bind(localStorage_); export const localStorage_removeItem = localStorage_.removeItem.bind(localStorage_); -export const indexedDB_ = window_.indexedDB; +export const indexedDB_ = window_.indexedDB || null; export const fetch_ = fetch; export const Error_ = msg => new Error(msg); export const Uint8Array_ = Uint8Array; diff --git a/src/etc/storage.js b/src/etc/storage.js index e8cefa4..69acc66 100644 --- a/src/etc/storage.js +++ b/src/etc/storage.js @@ -3,6 +3,7 @@ import { Object_entries, Object_keys, Promise_, + Promise_resolve, indexedDB_, localStorage_, localStorage_getItem, @@ -15,55 +16,55 @@ import { import './state.js'; let chunks_db = null; -if (indexedDB_) { - navigator_.storage?.persist?.(); - const request = indexedDB_.open('minicraft', 1); - request.onupgradeneeded = event => { - // migrate from localStorage to indexedDB - // if (event.oldVersion < 1) { - const store = event.target.result.createObjectStore('chunks', { - keyPath: ['world', 'coords'], - }); - for (const key of Object_keys(localStorage_)) { - if (key.startsWith('minicraft.world.')) { - const [world, coords] = key.substr(16).split(':'); - if (coords === 'meta') continue; - store.add({ - world: Number_(world), - coords, - data: localStorage_getItem(key), - }); - localStorage_removeItem(key); +export const chunks_db_promise = indexedDB_ +? new Promise_(resolve => { + navigator_.storage?.persist?.(); + const request = indexedDB_.open('minicraft', 1); + request.onupgradeneeded = event => { + // migrate from localStorage to indexedDB + // if (event.oldVersion < 1) { + const store = event.target.result.createObjectStore('chunks', { + keyPath: ['world', 'coords'], + }); + for (const key of Object_keys(localStorage_)) { + if (key.startsWith('minicraft.world.')) { + const [world, coords] = key.slice(16).split(':', 2); + if (coords === 'meta') continue; + store.add({ + world: Number_(world), + coords, + data: localStorage_getItem(key), + }); + localStorage_removeItem(key); + } } - } - }; - request.onsuccess = event => { - chunks_db = event.target.result; - }; -} + }; + request.onsuccess = event => { + chunks_db = event.target.result; + resolve(); + }; + }) +: Promise_resolve(); /** @param {number} world @param {string} coords @return {Promise} */ -export const chunk_get = async (world, coords) => { - if (!chunks_db) return localStorage_getItem(`minicraft.world.${world}:${coords}`); - - const request = chunks_db - .transaction('chunks', 'readonly') - .objectStore('chunks') - .get([ - world, - coords, - ]); - - return new Promise_(resolve => { - request.onsuccess = () => { - resolve(request.result?.data); - }; - }); -} +export const chunk_get = indexedDB_ +? (world, coords) => new Promise_(resolve => { + const request = chunks_db + .transaction('chunks', 'readonly') + .objectStore('chunks') + .get([ + world, + coords, + ]); + request.onsuccess = () => resolve(request.result?.data); + }) +: (world, coords) => Promise_resolve( + localStorage_getItem(`minicraft.world.${world}:${coords}`) + ) /** @param {number} world @@ -71,77 +72,61 @@ export const chunk_get = async (world, coords) => { @param {string} data @return {Promise} */ -export const chunk_set = async (world, coords, data) => { - if (!chunks_db) return localStorage_setItem(`minicraft.world.${world}:${coords}`, data); - - const request = chunks_db - .transaction('chunks', 'readwrite') - .objectStore('chunks') - .put({ - world, - coords, - data, - }); - - return new Promise_((resolve, reject) => { - request.onsuccess = () => { - resolve(); - }; - request.onerror = () => { - reject(request.error); - }; - }); -} +export const chunk_set = indexedDB_ +? (world, coords, data) => new Promise_((resolve, reject) => { + const request = chunks_db + .transaction('chunks', 'readwrite') + .objectStore('chunks') + .put({ + world, + coords, + data, + }); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }) +: (world, coords, data) => ( + localStorage_setItem(`minicraft.world.${world}:${coords}`, data), + Promise_resolve() + ) /** @param {number} world @param {string} coords @return {Promise} */ -export const chunk_delete = async (world, coords) => { - if (!chunks_db) return localStorage_removeItem(`minicraft.world.${world}:${coords}`); - - const request = chunks_db - .transaction('chunks', 'readwrite') - .objectStore('chunks') - .delete([ - world, - coords, - ]); - - return new Promise_(resolve => { - request.onsuccess = () => { - resolve(); - }; - }); -} +export const chunk_delete = indexedDB_ +? (world, coords) => new Promise_((resolve, reject) => { + const request = chunks_db + .transaction('chunks', 'readwrite') + .objectStore('chunks') + .delete([ + world, + coords, + ]); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }) +: (world, coords) => ( + localStorage_removeItem(`minicraft.world.${world}:${coords}`), + Promise_resolve() + ) /** @param {number} world @return {Promise>} */ -export const chunks_get = async world => { - const prefix = `minicraft.world.${world}:`; - if (!chunks_db) { - const chunks = {}; - const prefix_length = prefix.length; - for (const key of Object_keys(localStorage_)) { - if (key.startsWith(prefix)) { - chunks[key.substr(prefix_length)] = localStorage_getItem(key); - } - } - return chunks; - } - - const request = chunks_db - .transaction('chunks', 'readonly') - .objectStore('chunks') - .openCursor(); - const chunks = { - 'meta': localStorage_getItem(prefix + 'meta'), - }; - - return new Promise_(resolve => { +export const chunks_get = indexedDB_ +? world => new Promise_(resolve => { + const meta_data = localStorage_getItem(`minicraft.world.${world}:meta`); + if (!meta_data) return resolve({}); + const request = chunks_db + .transaction('chunks', 'readonly') + .objectStore('chunks') + .openCursor(); + const chunks = { + 'meta': meta_data, + }; request.onsuccess = () => { const cursor = request.result; if (cursor) { @@ -154,116 +139,91 @@ export const chunks_get = async world => { resolve(chunks); } }; - }); -} + }) +: world => { + const prefix = `minicraft.world.${world}:`; + const prefix_length = prefix.length; + const chunks = {}; + for (const key of Object_keys(localStorage_)) + if (key.startsWith(prefix)) { + chunks[key.slice(prefix_length)] = localStorage_getItem(key); + } + return Promise_resolve(chunks); + } /** @param {number} world @param {!Object} chunks @return {Promise} */ -export const chunks_set = async (world, chunks) => { - if (!chunks_db) { +export const chunks_set = indexedDB_ +? (world, chunks) => new Promise_((resolve, reject) => { + const transaction = chunks_db.transaction('chunks', 'readwrite'); + const store = transaction.objectStore('chunks'); + for (const [coords, data] of Object_entries(chunks)) { + if (coords !== 'meta') { + store.put({ + world, + coords, + data, + }); + } + else { + localStorage_setItem(`minicraft.world.${world}:meta`, data); + } + } + transaction.oncomplete = () => resolve(); + transaction.onerror = () => reject(transaction.error); + }) +: async (world, chunks) => { for (const [coords, data] of Object_entries(chunks)) { localStorage_setItem(`minicraft.world.${world}:${coords}`, data); } // assert margin for metadata localStorage_setItem('__margin', new Array(257).join('x')); localStorage_removeItem('__margin'); - return; } - const transaction = chunks_db.transaction('chunks', 'readwrite'); - const store = transaction.objectStore('chunks'); - for (const [coords, data] of Object_entries(chunks)) { - if (coords !== 'meta') { - store.put({ - world, - coords, - data, - }); - } - else { - localStorage_setItem(`minicraft.world.${world}:meta`, data); - } - } - - return new Promise_((resolve, reject) => { - transaction.oncomplete = () => { - resolve(); - }; - transaction.onerror = () => { - reject(transaction.error); - }; - }); -} - /** @param {number} world + @return {void} */ -export const chunks_delete = world => { - if (!chunks_db) { - const prefix = `minicraft.world.${world}:`; - for (const key of Object_keys(localStorage_)) { - if (key.startsWith(prefix)) { - localStorage_removeItem(key); +export const chunks_delete = indexedDB_ +? world => { + const request = chunks_db + .transaction('chunks', 'readwrite') + .objectStore('chunks') + .openCursor(); + request.onsuccess = () => { + const cursor = request.result; + if (cursor) { + if (cursor.value.world === world) { + cursor.delete(); + } + cursor.continue(); } } - return; + localStorage_removeItem(`minicraft.world.${world}:meta`); } - - localStorage_removeItem(`minicraft.world.${world}:meta`); - - const request = chunks_db - .transaction('chunks', 'readwrite') - .objectStore('chunks') - .openCursor(); - - request.onsuccess = () => { - const cursor = request.result; - if (cursor) { - if (cursor.value.world === world) { - cursor.delete(); - } - cursor.continue(); +: world => { + const prefix = `minicraft.world.${world}:`; + for (const key of Object_keys(localStorage_)) + if (key.startsWith(prefix)) { + localStorage_removeItem(key); } } -} /** @param {number} world_old @param {number} world_new + @return {Promise} */ -export const chunks_rename = async (world_old, world_new) => { - if (!chunks_db) { - const prefix_old = `minicraft.world.${world_old}:`; - const prefix_old_length = prefix_old.length; - const prefix_new = `minicraft.world.${world_new}:`; - for (const key of Object_keys(localStorage_)) { - if (key.startsWith(prefix_old)) { - const value = localStorage_getItem(key); - localStorage_removeItem(key); - localStorage_setItem( - prefix_new + key.substr(prefix_old_length), - value - ); - } - } - return; - } - - localStorage_setItem( - `minicraft.world.${world_new}:meta`, - localStorage_getItem(`minicraft.world.${world_old}:meta`) - ); - localStorage_removeItem(`minicraft.world.${world_old}:meta`); - - const store = chunks_db - .transaction('chunks', 'readwrite') - .objectStore('chunks'); - const request = store.openCursor(); - - return new Promise_(resolve => { +export const chunks_rename = indexedDB_ +? (world_old, world_new) => new Promise_(resolve => { + const store = chunks_db + .transaction('chunks', 'readwrite') + .objectStore('chunks'); + const request = store.openCursor(); request.onsuccess = () => { const cursor = request.result; if (cursor) { @@ -277,5 +237,24 @@ export const chunks_rename = async (world_old, world_new) => { resolve(); } }; - }); -} + localStorage_setItem( + `minicraft.world.${world_new}:meta`, + localStorage_getItem(`minicraft.world.${world_old}:meta`) + ); + localStorage_removeItem(`minicraft.world.${world_old}:meta`); + }) +: (world_old, world_new) => { + const prefix_old = `minicraft.world.${world_old}:`; + const prefix_old_length = prefix_old.length; + for (const key of Object_keys(localStorage_)) { + if (key.startsWith(prefix_old)) { + const value = localStorage_getItem(key); + localStorage_removeItem(key); + localStorage_setItem( + `minicraft.world.${world_new}:${key.slice(prefix_old_length)}`, + value + ); + } + } + return Promise_resolve(); + } diff --git a/src/game/m_game.js b/src/game/m_game.js index b42b12c..302e77e 100644 --- a/src/game/m_game.js +++ b/src/game/m_game.js @@ -648,7 +648,7 @@ export const game_render = (model, now) => { const coord_part_parse = (base, value) => ( value = ( value.startsWith('~') - ? base + Number_(value.substr(1)) + ? base + Number_(value.slice(1)) : Number_(value) ), isNaN(value) @@ -663,7 +663,7 @@ export const game_message_send = (model, value) => { } = model; if (!value) {} else if (value.charAt(0) === '/') { - const args = value.substr(1).split(' '); + const args = value.slice(1).split(' '); const command = args.shift(); switch(command) { case 'clear': diff --git a/src/game/m_world.js b/src/game/m_world.js index 682af9a..cee61f5 100644 --- a/src/game/m_world.js +++ b/src/game/m_world.js @@ -359,14 +359,16 @@ export const world_save = (model, player) => { } export const world_load = (model, player) => { - const meta = localStorage_getItem(`minicraft.world.${model.id}:meta`); + const meta = /** @type {TYPE_WORLD_META} */ (JSON_parse( + localStorage_getItem(`minicraft.world.${model.id}:meta`) + )); if (meta) { const { p, s, t, v, - } = /** @type {TYPE_WORLD_META} */ (JSON_parse(meta)); + } = meta; if (v == null) return; diff --git a/src/os/c_window.js b/src/os/c_window.js index f0ee736..b24635c 100644 --- a/src/os/c_window.js +++ b/src/os/c_window.js @@ -49,7 +49,7 @@ const AREA_CENTER = 4; const AREA_RIGHT = 5; const AREA_BOTTOMLEFT = 6; const AREA_BOTTOM = 7; -//const AREA_BOTTOMRIGHT = 8; +const AREA_BOTTOMRIGHT = 8; const CURSORS = 'nwse-resize,ns-resize,nesw-resize,ew-resize,move,ew-resize,nesw-resize,ns-resize,nwse-resize'.split(','); @@ -120,26 +120,28 @@ const drag_handler = (hook_model_state_actions, window_id, event) => { return; } event.preventDefault(); - element.style.willChange = 'transform'; document_.activeElement.blur(); const [window_state_start, window_actions] = hook_model_state_actions; const mouse_x_start = event.clientX; const mouse_y_start = event.clientY; let mouse_x_start_relative = mouse_x_start - window_state_start.left; const mouse_y_start_relative = mouse_y_start - window_state_start.top; - const area = ( // [0..7], [8..n-9], [n-8..n-1] + const area_h = ( // [0..7], [8..n-9], [n-8..n-1] mouse_x_start_relative < 8 ? AREA_START : mouse_x_start_relative < window_state_start.width - 8 ? AREA_MIDDLE : AREA_END - ) + ( + ); + const area_v = ( mouse_y_start_relative < 8 - ? AREA_START * 3 + ? AREA_START : mouse_y_start_relative < window_state_start.height - 8 - ? AREA_MIDDLE * 3 - : AREA_END * 3 + ? AREA_MIDDLE + : AREA_END ); + const area = area_h + area_v * 3; + element.style.willChange = area === AREA_CENTER ? 'transform' : 'contents'; actions.state_patch({ cursor: CURSORS[area], }); @@ -150,48 +152,8 @@ const drag_handler = (hook_model_state_actions, window_id, event) => { let mouse_x_now = event.clientX; let mouse_y_now = event.clientY; - // lock x/y movement - switch (area) { - case AREA_TOP: - case AREA_BOTTOM: - mouse_x_now = mouse_x_start; - break; - case AREA_LEFT: - case AREA_RIGHT: - mouse_y_now = mouse_y_start; - } - - // moving left edge, limit x - switch (area) { - case AREA_TOPLEFT: - case AREA_LEFT: - case AREA_BOTTOMLEFT: - mouse_x_now = Math_max( - mouse_x_start_relative, - Math_min( - mouse_x_now, - window_state_start.left + window_state_start.width - WIDTH_MIN + mouse_x_start_relative - ) - ); - } - - // moving top edge, limit y - switch (area) { - case AREA_TOPLEFT: - case AREA_TOP: - case AREA_TOPRIGHT: - mouse_y_now = Math_max( - mouse_y_start_relative, - Math_min( - mouse_y_now, - window_state_start.top + window_state_start.height - HEIGHT_MIN + mouse_y_start_relative - ) - ); - } - - // move/resize - switch (area) { - case AREA_CENTER: + // move only? + area_center: if (area === AREA_CENTER) { // snap to edges if (event.type === 'mouseup') { if (mouse_y_now < 4) { @@ -201,7 +163,7 @@ const drag_handler = (hook_model_state_actions, window_id, event) => { width: app_state.screen_width, height: app_state.screen_height, }); - break; + break area_center; } if (mouse_x_now < 4) { window_actions.patch({ @@ -210,7 +172,7 @@ const drag_handler = (hook_model_state_actions, window_id, event) => { width: Math_ceil(app_state.screen_width / 2), height: app_state.screen_height, }); - break; + break area_center; } if (mouse_x_now > app_state.screen_width - 3) { window_actions.patch({ @@ -219,7 +181,7 @@ const drag_handler = (hook_model_state_actions, window_id, event) => { width: Math_floor(app_state.screen_width / 2), height: app_state.screen_height, }); - break; + break area_center; } } // unsnap from edges @@ -244,47 +206,54 @@ const drag_handler = (hook_model_state_actions, window_id, event) => { mouse_x_now - mouse_x_start_relative, mouse_y_now - mouse_y_start_relative ); - break; - // those lack perfection, 5€ for who finds a better solution - case AREA_TOPLEFT: - case AREA_TOP: - case AREA_LEFT: - window_actions.move_to( - mouse_x_now - mouse_x_start_relative, - mouse_y_now - mouse_y_start_relative - ); - window_actions.resize_to( - window_state_start.left + window_state_start.width + mouse_x_start_relative - mouse_x_now, - window_state_start.top + window_state_start.height + mouse_y_start_relative - mouse_y_now - ); - break; - case AREA_TOPRIGHT: - window_actions.move_to( - window_state_start.left, - mouse_y_now - mouse_y_start_relative - ); - window_actions.resize_to( - window_state_start.width - mouse_x_start + mouse_x_now, - window_state_start.top + window_state_start.height + mouse_y_start_relative - mouse_y_now - ); - break; - case AREA_BOTTOMLEFT: - window_actions.move_to( - mouse_x_now - mouse_x_start_relative, - window_state_start.top - ); - window_actions.resize_to( - window_state_start.left + window_state_start.width + mouse_x_start_relative - mouse_x_now, - window_state_start.height - mouse_y_start + mouse_y_now - ); - break; - //case AREA_RIGHT: - //case AREA_BOTTOM: - //case AREA_BOTTOMRIGHT: - default: + } + // resize? + else { + // need to move? + if (area_h === AREA_START || area_v === AREA_START) { + window_actions.move_to( + // left + area_h === AREA_START + ? ( + // limit mouse_x to left and right + mouse_x_now = Math_max( + mouse_x_start_relative, + Math_min( + mouse_x_now, + window_state_start.left + window_state_start.width - WIDTH_MIN + mouse_x_start_relative + ) + ) + ) - mouse_x_start_relative + : window_state_start.left, + // top + area_v === AREA_START + ? ( + // limit mouse_y to top and bottom + mouse_y_now = Math_max( + mouse_y_start_relative, + Math_min( + mouse_y_now, + window_state_start.top + window_state_start.height - HEIGHT_MIN + mouse_y_start_relative + ) + ) + ) - mouse_y_start_relative + : window_state_start.top + ); + } + window_actions.resize_to( - window_state_start.width - mouse_x_start + mouse_x_now, - window_state_start.height - mouse_y_start + mouse_y_now + // width + area_h === AREA_START + ? window_state_start.width + window_state_start.left + mouse_x_start_relative - mouse_x_now + : area_h === AREA_MIDDLE + ? window_state_start.width + : window_state_start.width - mouse_x_start + mouse_x_now, + // height + area_v === AREA_START + ? window_state_start.height + window_state_start.top + mouse_y_start_relative - mouse_y_now + : area_v === AREA_MIDDLE + ? window_state_start.height + : window_state_start.height - mouse_y_start + mouse_y_now ); } defer_end(); From cf2110976bfb0520a1f2e2c107f7470187b8d59f Mon Sep 17 00:00:00 2001 From: L3P3 Date: Thu, 21 Aug 2025 14:53:53 +0200 Subject: [PATCH 2/8] fix build optimization caused bug --- build.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build.js b/build.js index d5fb5bd..e362cde 100644 --- a/build.js +++ b/build.js @@ -122,7 +122,9 @@ async function build_js(lang) { (await readFile(TMP_FILE, 'ascii')) .split('content').join('c') .split('loaded').join('l') - .split('downl').join('downloaded'), + .split('"transform":"cs"').join('"transform":"contents"') + .split('downl').join('downloaded') + .split('downloadedoading').join('downloading'), 'ascii' ); From 4dcb5f4814d3ec7d4c96081cbdb01a12d1fd7d84 Mon Sep 17 00:00:00 2001 From: L3P3 Date: Mon, 29 Dec 2025 16:33:32 +0100 Subject: [PATCH 3/8] contants, comments, array alias --- src/app.js | 2 +- src/etc/constants.js | 3 +++ src/etc/helpers.js | 7 ++++--- src/etc/state.js | 3 ++- src/etc/storage.js | 3 ++- src/game/c_menu_start.js | 8 +++++--- src/game/m_player.js | 3 ++- src/game/m_world_store.js | 26 ++++++++++++++++++-------- 8 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/app.js b/src/app.js index bbb7fc0..2f9f625 100755 --- a/src/app.js +++ b/src/app.js @@ -173,7 +173,7 @@ function Root() { node_dom('div[className=cursormask]', { S: { cursor: app_state.cursor, - } + }, }), ]; } diff --git a/src/etc/constants.js b/src/etc/constants.js index cc0655b..a82abcc 100644 --- a/src/etc/constants.js +++ b/src/etc/constants.js @@ -19,6 +19,9 @@ export const COORDINATE_OFFSET = 1 << 16; export const WORLD_FORMAT = 1; +export const WORLD_STORED_NOT = 0; +export const WORLD_STORED_SHOULD = 1; + export const BLOCK_TYPE_AIR = 0; export const BLOCK_TYPE_STONE = 1; export const BLOCK_TYPE_GRASS = 2; diff --git a/src/etc/helpers.js b/src/etc/helpers.js index 99ac615..c8c7d02 100644 --- a/src/etc/helpers.js +++ b/src/etc/helpers.js @@ -36,6 +36,7 @@ export const localStorage_removeItem = localStorage_.removeItem.bind(localStorag export const indexedDB_ = window_.indexedDB || null; export const fetch_ = fetch; export const Error_ = msg => new Error(msg); +export const Array_ = Array; export const Uint8Array_ = Uint8Array; export const Uint32Array_ = Uint32Array; export const Set_ = Set; @@ -67,11 +68,11 @@ export const number_toFixed2 = num => num.toFixed(2); @param {string=} pad @return {string} @noinline + @suppress {checkTypes} */ export const number_padStart2 = (num, pad) => ( - Math_floor(num) - .toString() - .padStart(2, pad) + num = Math_floor(num).toString(), + num.length < 2 ? pad + num : num ); /** diff --git a/src/etc/state.js b/src/etc/state.js index 2046e5c..306fdd9 100644 --- a/src/etc/state.js +++ b/src/etc/state.js @@ -7,6 +7,7 @@ import { WINDOW_MODE_FULL, WINDOW_TYPE_EMPTY, WINDOW_TYPE_GAME, + WORLD_STORED_NOT, } from './constants.js'; import { VERSION, @@ -117,7 +118,7 @@ const reducers = { prompt_(locale_unknown_world_found, '') || locale_unknown_world ).substring(0, 16), mod_l: Date_now(), - mod_r: 0, + mod_r: WORLD_STORED_NOT, }; needs_save = true; } diff --git a/src/etc/storage.js b/src/etc/storage.js index 69acc66..861a9ac 100644 --- a/src/etc/storage.js +++ b/src/etc/storage.js @@ -1,4 +1,5 @@ import { + Array_, Number_, Object_entries, Object_keys, @@ -180,7 +181,7 @@ export const chunks_set = indexedDB_ localStorage_setItem(`minicraft.world.${world}:${coords}`, data); } // assert margin for metadata - localStorage_setItem('__margin', new Array(257).join('x')); + localStorage_setItem('__margin', Array_(257).join('x')); localStorage_removeItem('__margin'); } diff --git a/src/game/c_menu_start.js b/src/game/c_menu_start.js index 0bdf5f3..e81a498 100644 --- a/src/game/c_menu_start.js +++ b/src/game/c_menu_start.js @@ -11,6 +11,8 @@ import { import { APP_VIEW_GAME, APP_VIEW_SETTINGS, + WORLD_STORED_NOT, + WORLD_STORED_SHOULD, } from '../etc/constants.js'; import { API, @@ -266,7 +268,7 @@ export default function MenuStart({ id: Math_min(0, ...config.worlds.map(world => world.id)) - 1, label: name, mod_l: Date_now(), - mod_r: 0, + mod_r: WORLD_STORED_NOT, }); } }), @@ -499,13 +501,13 @@ export default function MenuStart({ actions.world_add({ id: world_selected.id, label: world_selected.label, - mod_l: 1, + mod_l: WORLD_STORED_SHOULD, mod_r: world_selected.remote, }); } else if (!world_selected.remote) { actions.world_prop(world_selected.id, { - mod_r: 1, + mod_r: WORLD_STORED_SHOULD, }); } }, diff --git a/src/game/m_player.js b/src/game/m_player.js index 284fb1d..1a844cd 100644 --- a/src/game/m_player.js +++ b/src/game/m_player.js @@ -4,6 +4,7 @@ import { PLAYER_INVENTORY, } from '../etc/constants.js'; import { + Array_, Math_cos, Math_max, Math_min, @@ -33,7 +34,7 @@ export const player_create = (world, account) => ({ block_select_b: null, gamemode: GAMEMODE_CREATIVE, health: 20, - inventory: new Array(PLAYER_INVENTORY) + inventory: Array_(PLAYER_INVENTORY) .fill(null) .map(() => slot_create(null)), mouse_mode: MOUSE_MODE_NORMAL, diff --git a/src/game/m_world_store.js b/src/game/m_world_store.js index 761b133..c4332b7 100644 --- a/src/game/m_world_store.js +++ b/src/game/m_world_store.js @@ -2,6 +2,10 @@ import { defer, } from '../etc/lui.js'; +import { + WORLD_STORED_NOT, + WORLD_STORED_SHOULD, +} from '../etc/constants.js'; import { API, API_DATA, @@ -124,7 +128,7 @@ export const world_store_lists_merge = config => { hash: world.hash, id: world.id, label: world.label, - local: 0, + local: WORLD_STORED_NOT, public: world.public, remote: world.modified, writable: world.writable, @@ -134,6 +138,7 @@ export const world_store_lists_merge = config => { for (const world_local of /** @type {!Array} */ (config.worlds)) { const world_list_item = world_list.find(world => world.id === world_local.id); + // present on remote? if (world_list_item) { const last_change_here = world_list_item.local = world_local.mod_l; const last_change_there = world_list_item.remote; @@ -162,9 +167,10 @@ export const world_store_lists_merge = config => { } } } + // not present on remote? else { if ( - world_local.mod_r > 1 && + world_local.mod_r > WORLD_STORED_SHOULD && world_list_remote ) { alert_( @@ -174,7 +180,7 @@ export const world_store_lists_merge = config => { ); defer(); actions.world_prop(world_local.id, { - mod_r: 0, + mod_r: WORLD_STORED_NOT, }); } @@ -185,7 +191,7 @@ export const world_store_lists_merge = config => { label: world_local.label, local: world_local.mod_l, public: false, - remote: world_local.mod_r === 1 ? 1 : 0, + remote: world_local.mod_r === WORLD_STORED_SHOULD ? WORLD_STORED_SHOULD : WORLD_STORED_NOT, writable: true, }); } @@ -241,7 +247,8 @@ const world_store_sync = async world => { world_syncing: id, }); - if (world.local < world.remote) { // download + // download? + if (world.local < world.remote) { try { const response = await fetch_(`${API_DATA}worlds/${world.hash}.json`); const json = await response.json(); @@ -264,13 +271,16 @@ const world_store_sync = async world => { } } } - else if (!world.writable) { // upload not allowed + // upload not allowed? + else if (!world.writable) { defer(); + // set local modification time to remote to prevent further sync attempts actions.world_prop(id, { mod_l: app_state.config.worlds.find(world => world.id === id).mod_r, }); } - else { // upload + // upload? + else { try { let id_new = id; if (world.remote === 1) { // register new world @@ -328,7 +338,7 @@ const world_store_sync = async world => { catch (error) { defer(); actions.world_prop(id, { - mod_r: 0, + mod_r: WORLD_STORED_NOT, }); throw Error_(locale_error_upload_world + error.message); } From d6c36682c65b89952db863d2ba724adcbc79d652 Mon Sep 17 00:00:00 2001 From: L3P3 Date: Tue, 6 Jan 2026 12:52:32 +0100 Subject: [PATCH 4/8] wip: improve sync --- package.json | 2 +- src/app.js | 38 +++++++++++------ src/etc/helpers.js | 23 +++++++++++ src/game/c_app.js | 20 --------- src/game/c_menu_start.js | 64 +++++++++++----------------- src/game/m_game.js | 13 +----- src/game/m_world.js | 24 ++++++++--- src/game/m_world_store.js | 87 ++++++++++++++++++--------------------- 8 files changed, 133 insertions(+), 138 deletions(-) diff --git a/package.json b/package.json index 66def5e..2c557d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minicraft", - "version": "0.11.8", + "version": "0.11.9", "description": "voxel-based 3d game, written in javascript", "homepage": "https://l3p3.de/minicraft", "repository": { diff --git a/src/app.js b/src/app.js index 2f9f625..69627c5 100755 --- a/src/app.js +++ b/src/app.js @@ -50,6 +50,7 @@ import { } from './game/m_renderer.js'; import { world_store_init, + world_store_sync_check, } from './game/m_world_store.js'; import Window from './os/c_window.js'; @@ -62,6 +63,7 @@ export const in_event_set = value => { in_event = value; } let last_touch_event = 0; +let sync_check_timeout = 0; function Root() { hook_app_state(); @@ -158,6 +160,16 @@ function Root() { }, [flag_touch]); hook_effect(tiles_set, [app_state.config.textures]); + hook_effect(() => { + clearTimeout_(sync_check_timeout); + if (!app_state.connection_error) { + sync_check_timeout = setTimeout_(world_store_sync_check); + } + }, [ + app_state.connection_error, + app_state.worlds_merged, + ]); + hook_dom('', { onkeydown: handler_key, onkeyup: handler_key, @@ -209,21 +221,23 @@ else if (BroadcastChannel_) { channel_lock.postMessage('anyone there?'); } else { - // first make sure lock has expired const lock_found = Number_(localStorage_getItem('minicraft.lock')); - const lock_limit = Date_now() - 1000; - // if already expired - if (lock_found < lock_limit) { + const lock_limit = Date_now() - 1500; + // if lock still valid, wait and check again + if (lock_found > lock_limit) { + setTimeout_(() => { + // chunks_db_promise will already be resolved by then + init( + Number_(localStorage_getItem('minicraft.lock')) === lock_found + ? Root + : ErrorOpened + ); + }, lock_found - lock_limit); + } + // if not, init right away + else { chunks_db_promise.then(() => init(Root) ) } - // if not, wait and check again - else setTimeout_(() => { - init( - Number_(localStorage_getItem('minicraft.lock')) === lock_found - ? Root - : ErrorOpened - ); - }, lock_found - lock_limit); } diff --git a/src/etc/helpers.js b/src/etc/helpers.js index c8c7d02..c8e9da8 100644 --- a/src/etc/helpers.js +++ b/src/etc/helpers.js @@ -1,4 +1,6 @@ import { + locale_error_connection, + locale_error_no_permission_logged_in, locale_today, } from '../etc/locale.js'; @@ -56,6 +58,16 @@ export const confirm_ = confirm; export const prompt_ = prompt; export const flag_chromium = navigator_.userAgent.includes('WebKit'); +// old safari does not support fill for typed arrays +/*if (!Uint32Array_.prototype.fill) { + Uint32Array_.prototype.fill = Array_.prototype.fill; +}*/ + +export const headers_json_post = { + method: 'POST', + headers: {'Content-Type': 'application/json'}, +}; + /** @param {number} num @return {string} @@ -132,3 +144,14 @@ export const datify = (time, short) => { return result + date_then.getHours() + ':' + number_padStart2(date_then.getMinutes(), '0'); } + +export const response_parse = response => { + if (!response.ok) { + throw Error_( + response.status === 403 + ? locale_error_no_permission_logged_in + : locale_error_connection + ); + } + return response.json(); +} diff --git a/src/game/c_app.js b/src/game/c_app.js index 669e0a0..980f073 100644 --- a/src/game/c_app.js +++ b/src/game/c_app.js @@ -11,10 +11,6 @@ import { APP_VIEW_SETTINGS, APP_VIEW_WORLDS, } from '../etc/constants.js'; -import { - clearTimeout_, - setTimeout_, -} from '../etc/helpers.js'; import { locale_error_connection, locale_retry, @@ -23,16 +19,10 @@ import { actions, } from '../etc/state.js'; -import { - world_store_sync_check, -} from './m_world_store.js'; - import Game from './c_game.js'; import MenuStart from './c_menu_start.js'; import Settings from './c_settings.js'; -let sync_check_timeout = 0; - export default function App({ key_event, state, @@ -51,16 +41,6 @@ export default function App({ ); }, [view]); - hook_effect(() => { - clearTimeout_(sync_check_timeout); - if (!state.connection_error) { - sync_check_timeout = setTimeout_(world_store_sync_check); - } - }, [ - state.connection_error, - state.worlds_merged, - ]); - const frame = hook_dom('div[className=game]'); return [ diff --git a/src/game/c_menu_start.js b/src/game/c_menu_start.js index e81a498..96212e3 100644 --- a/src/game/c_menu_start.js +++ b/src/game/c_menu_start.js @@ -21,7 +21,6 @@ import { import { alert_, Date_now, - Error_, JSON_stringify, Math_max, Math_min, @@ -29,6 +28,8 @@ import { datify, fetch_, prompt_, + response_parse, + headers_json_post, } from '../etc/helpers.js'; import { locale_ask_world_delete_1, @@ -39,13 +40,11 @@ import { locale_delete_world, locale_download_world_from_server, locale_download, - locale_error_connection, locale_error_delete_world, locale_error_edit_world, locale_error_list_is_loading, locale_error_name_too_long, locale_error_no_permission, - locale_error_no_permission_logged_in, locale_error_no_world_selected, locale_error_not_logged_in, locale_error_world_is_loading, @@ -339,22 +338,14 @@ export default function MenuStart({ if (world_selected.remote) { busy_set(true); fetch_(API + 'world', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, + ...headers_json_post, body: JSON_stringify({ what: 'meta', world: world_selected.id, label: name, }), }) - .then(response => { - if (!response.ok) throw Error_( - response.status === 403 - ? locale_error_no_permission_logged_in - : locale_error_connection - ); - return response.json(); - }) + .then(response_parse) .catch(error => { alert_(locale_error_edit_world + error.message); }) @@ -401,20 +392,16 @@ export default function MenuStart({ world: world_selected.id, }), }) - .then(response => { - if (!response.ok) throw Error_( - response.status === 403 - ? locale_error_no_permission_logged_in - : locale_error_connection - ); - defer(); - world_selected_id_set(null); - menu_opened_set(false); - world_store_remote_reload(); - busy_set(false); - defer_end(); - return response.json(); - }) + .then(response => ( + response = response_parse(response), + defer(), + world_selected_id_set(null), + menu_opened_set(false), + world_store_remote_reload(), + busy_set(false), + defer_end(), + response + )) .catch(error => { alert_(locale_error_delete_world + error.message); busy_set(false); @@ -446,26 +433,21 @@ export default function MenuStart({ onclick: () => { busy_set(true); fetch_(API + 'world', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, + ...headers_json_post, body: JSON_stringify({ what: 'meta', world: world_selected.id, public: !world_selected.public, }), }) - .then(response => { - if (!response.ok) throw Error_( - response.status === 403 - ? locale_error_no_permission_logged_in - : locale_error_connection - ); - defer(); - world_store_remote_reload(); - busy_set(false); - defer_end(); - return response.json(); - }) + .then(response => ( + response = response_parse(response), + defer(), + world_store_remote_reload(), + busy_set(false), + defer_end(), + response + )) .catch(error => { alert_(locale_error_edit_world + error.message); busy_set(false); diff --git a/src/game/m_game.js b/src/game/m_game.js index 302e77e..4546fa0 100644 --- a/src/game/m_game.js +++ b/src/game/m_game.js @@ -1,5 +1,4 @@ import { - defer, now, } from '../etc/lui.js'; @@ -61,9 +60,9 @@ import { import { clearInterval_, clearTimeout_, - Date_now, fetch_, flag_chromium, + headers_json_post, JSON_stringify, Math_ceil, Math_floor, @@ -205,12 +204,7 @@ export const game_renderer_init = (model, canvas_element) => ( ) export const game_save = model => { - if (model.world.flag_frozen) return; - world_save(model.world, model.player); - actions.world_prop(model.world.id, { - mod_l: Date_now(), - }); } export const game_resolution_update = model => { @@ -952,10 +946,7 @@ const game_poll = (model, msg) => ( ( msg ? fetch_(API_CHAT, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + ...headers_json_post, body: JSON_stringify(/** @type {TYPE_CHAT_API} */ ({ msg, })), diff --git a/src/game/m_world.js b/src/game/m_world.js index cee61f5..229f453 100644 --- a/src/game/m_world.js +++ b/src/game/m_world.js @@ -15,6 +15,7 @@ import { WORLD_FORMAT, } from '../etc/constants.js'; import { + Date_now, JSON_parse, JSON_stringify, localStorage_getItem, @@ -68,6 +69,8 @@ export const world_create = id => { chunks_checklist: null, // next checklist item to check chunks_checklist_index: 0, + // could have been modified since last save + flag_dirty: false, // nothing must change flag_frozen: !app_state.worlds_merged.find(i => i.id === id).writable, // currently centered chunk (relative chunk position inside superchunk) @@ -174,8 +177,8 @@ export const world_block_set_try = (model, x, y, z, value) => { return true; } -export const world_data_init = (model, player, size_l2) => { - if (model.chunks) world_save(model, player); +export const world_data_init = async (model, player, size_l2) => { + if (model.chunks) await world_save(model, player); const size = 1 << ( model.size_l2 = size_l2 @@ -311,8 +314,14 @@ const world_chunk_load_setup = model => { model.chunks_checklist_index = 0; } -export const world_save = (model, player) => { - if (model.flag_frozen) return; +export const world_save = async (model, player) => { + if ( + model.flag_frozen || + !model.flag_dirty + ) return; + + model.flag_dirty = false; + const mod_l = Date_now(); const i = player.inventory.map(({content}) => content && [ @@ -351,11 +360,15 @@ export const world_save = (model, player) => { })) ); - return Promise_.all( + await Promise_.all( model.chunks .filter(chunk => chunk.dirty) .map(chunk => world_chunk_save(model, chunk)) ); + + actions.world_prop(model.id, { + mod_l, + }); } export const world_load = (model, player) => { @@ -589,6 +602,7 @@ export const world_chunk_load = async (model, all) => { } export const world_tick = (model, player) => { + model.flag_dirty = true; world_time_set(model, model.time + 1); world_offset_update(model, player, false); diff --git a/src/game/m_world_store.js b/src/game/m_world_store.js index c4332b7..97b7e42 100644 --- a/src/game/m_world_store.js +++ b/src/game/m_world_store.js @@ -18,8 +18,10 @@ import { datify, Error_, fetch_, + headers_json_post, JSON_stringify, Math_max, + response_parse, setTimeout_, } from '../etc/helpers.js'; import { @@ -27,10 +29,8 @@ import { locale_error_conflict_2, locale_error_conflict_3, locale_error_conflict_4, - locale_error_connection, locale_error_download_world, locale_error_loading_worldlist, - locale_error_no_permission_logged_in, locale_error_storage, locale_error_upload_world, locale_warn_world_remote_missing_1, @@ -78,17 +78,16 @@ const world_list_remote_load = async initial => { try { const response = await fetch_(`${API}world?what=${initial ? 'initia' : 'meta_al'}l`); - if (!response.ok) throw Error_(locale_error_connection); - const json = await response.json(); + const result = await response_parse(response); if (!initial) { - world_list_remote = /** @type {!Array} */ (json); + world_list_remote = /** @type {!Array} */ (result); actions.state_patch({ world_list_loading: false, worlds_merged: world_store_lists_merge(app_state.config), }); } else { - const json_initial = /** @type {TYPE_RESPONSE_INITIAL} */ (json); + const json_initial = /** @type {TYPE_RESPONSE_INITIAL} */ (result); if ( VERSION !== 'dev' && json_initial.version_latest !== VERSION @@ -178,10 +177,11 @@ export const world_store_lists_merge = config => { world_local.label + locale_warn_world_remote_missing_2 ); - defer(); + debugger; + /*defer(); actions.world_prop(world_local.id, { mod_r: WORLD_STORED_NOT, - }); + });*/ } world_list.push({ @@ -191,7 +191,11 @@ export const world_store_lists_merge = config => { label: world_local.label, local: world_local.mod_l, public: false, - remote: world_local.mod_r === WORLD_STORED_SHOULD ? WORLD_STORED_SHOULD : WORLD_STORED_NOT, + remote: ( + world_local.mod_r === WORLD_STORED_SHOULD + ? WORLD_STORED_SHOULD + : WORLD_STORED_NOT + ), writable: true, }); } @@ -212,8 +216,8 @@ export const world_store_sync_check = async () => { try { while ( world_syncable = world_list_merged.find(world => - world.local > 0 && - world.remote > 0 && + world.local > WORLD_STORED_NOT && + world.remote > WORLD_STORED_NOT && world.local !== world.remote ) ) { @@ -232,11 +236,6 @@ export const world_store_sync_check = async () => { syncing = false; } -const headers_json_post = { - method: 'POST', - headers: {'Content-Type': 'application/json'}, -}; - /** download/upload world only call this when the modification time actually differs @@ -251,8 +250,8 @@ const world_store_sync = async world => { if (world.local < world.remote) { try { const response = await fetch_(`${API_DATA}worlds/${world.hash}.json`); - const json = await response.json(); - await chunks_set(id, /** @type {!Object} */ (json)); + const result = await response.json(); + await chunks_set(id, /** @type {!Object} */ (result)); defer(); actions.world_prop(id, { mod_l: world.remote, @@ -282,57 +281,49 @@ const world_store_sync = async world => { // upload? else { try { + const data_promise = chunks_get(id); let id_new = id; - if (world.remote === 1) { // register new world - const response = await fetch_(API + 'world', { + + let rename_promise = null; + // world must be registered first? + if (world.remote === WORLD_STORED_SHOULD) { + const response_register = await fetch_(API + 'world', { ...headers_json_post, body: JSON_stringify({ what: 'meta', label: world.label, }), }); - if (!response.ok) throw Error_( - response.status === 403 - ? locale_error_no_permission_logged_in - : locale_error_connection + const result_register = await response_parse(response_register); + rename_promise = chunks_rename( + world_renamed_id_old = id, + world_renamed_id_new = id_new = result_register.id ); - const json = await response.json(); - id_new = json.id; } - const json = await chunks_get(id); - const response = await fetch_(API + 'world', { + const response_upload = await fetch_(API + 'world', { ...headers_json_post, body: JSON_stringify({ what: 'data', world: id_new, - data: json, + data: await data_promise, }), }); - if (!response.ok) throw Error_( - response.status === 403 - ? locale_error_no_permission_logged_in - : locale_error_connection - ); - const result = await response.json(); + const result_upload = await response_parse(response_upload); defer(); - if (id_new === id) { + if (world.remote === WORLD_STORED_SHOULD) { actions.world_prop(id, { - mod_l: result.modified, - mod_r: result.modified, + id: id_new, + mod_l: result_upload.modified, + mod_r: result_upload.modified, }); + await rename_promise; } - else { // replace the id - await chunks_rename(id, id_new); - actions.world_remove(id); - actions.world_add({ - id: id_new, - label: world.label, - mod_l: result.modified, - mod_r: result.modified, + else { + actions.world_prop(id, { + mod_l: result_upload.modified, + mod_r: result_upload.modified, }); - world_renamed_id_old = id; - world_renamed_id_new = id_new; } } catch (error) { From de4b605c673c5a33ae3f9bdec6142ac4148c8838 Mon Sep 17 00:00:00 2001 From: L3P3 Date: Sun, 11 Jan 2026 14:11:59 +0100 Subject: [PATCH 5/8] #60 fix: world upload --- src/etc/state.js | 44 ++++------- src/game/c_menu_start.js | 7 +- src/game/c_settings.js | 9 +-- src/game/m_game.js | 7 +- src/game/m_world_store.js | 149 +++++++++++++++++++++----------------- 5 files changed, 105 insertions(+), 111 deletions(-) diff --git a/src/etc/state.js b/src/etc/state.js index 306fdd9..449ac0a 100644 --- a/src/etc/state.js +++ b/src/etc/state.js @@ -145,8 +145,8 @@ const reducers = { world_list_cooldown: true, world_list_loading: true, world_syncing: null, - worlds_merged: world_store_lists_merge(config), worlds_opened: [], + ...world_store_lists_merge(config), }; if (needs_save) { state.config_saved = null; @@ -235,35 +235,28 @@ const reducers = { ], } ), - world_add: (state, world) => { - const config = { + world_add: (state, world) => ({ + ...state, + ...world_store_lists_merge({ ...state.config, worlds: [ ...state.config.worlds, world, ], - }; - return { - ...state, - config, - worlds_merged: world_store_lists_merge(config), - }; - }, - world_remove: (state, id) => { - const config = { + }), + }), + world_remove: (state, id) => ({ + ...state, + ...world_store_lists_merge({ ...state.config, worlds: state.config.worlds.filter( world => world.id !== id ), - }; - return { - ...state, - config, - worlds_merged: world_store_lists_merge(config), - }; - }, - world_prop: (state, id, patch) => { - const config = { + }), + }), + world_prop: (state, id, patch) => ({ + ...state, + ...world_store_lists_merge({ ...state.config, worlds: state.config.worlds.map(world => ( world.id === id @@ -273,13 +266,8 @@ const reducers = { } : world )), - }; - return { - ...state, - config, - worlds_merged: world_store_lists_merge(config), - }; - }, + }), + }), }; export let app_state; diff --git a/src/game/c_menu_start.js b/src/game/c_menu_start.js index 96212e3..61d0c97 100644 --- a/src/game/c_menu_start.js +++ b/src/game/c_menu_start.js @@ -379,8 +379,8 @@ export default function MenuStart({ locale_ask_world_delete_1 + world_selected.label + locale_ask_world_delete_2 )) return; if (world_selected.local) { - actions.world_remove(world_selected.id); chunks_delete(world_selected.id); + actions.world_remove(world_selected.id); } else { busy_set(true); @@ -393,14 +393,13 @@ export default function MenuStart({ }), }) .then(response => ( - response = response_parse(response), + response_parse(response), defer(), world_selected_id_set(null), menu_opened_set(false), world_store_remote_reload(), busy_set(false), - defer_end(), - response + defer_end() )) .catch(error => { alert_(locale_error_delete_world + error.message); diff --git a/src/game/c_settings.js b/src/game/c_settings.js index 809dcb4..bb38f84 100644 --- a/src/game/c_settings.js +++ b/src/game/c_settings.js @@ -13,6 +13,7 @@ import { import { Promise_, fetch_, + response_parse, } from '../etc/helpers.js'; import { API_DATA, @@ -71,12 +72,8 @@ export default function Settings({ const textures = hook_async(() => ( !textures_opened ? Promise_.resolve(null) : fetch_(`${API_DATA}textures.json`) - .then(response => ( - response.ok - ? response.json() - : null - )) - .catch(e => null) + .then(response_parse) + .catch(_error => null) ), [textures_opened], null); hook_dom('div[className=menu overlay]'); diff --git a/src/game/m_game.js b/src/game/m_game.js index 4546fa0..6d6ba1f 100644 --- a/src/game/m_game.js +++ b/src/game/m_game.js @@ -255,10 +255,7 @@ export const game_menu_close = model => { */ export const game_mouse_catch = async model => { if (model.config.flag_touch) return; - try { - await model.frame_element.requestPointerLock(); - } - catch (error) {} + return model.frame_element.requestPointerLock().catch(_error => {}); } export const game_mouse_move_player = (model, event) => { @@ -969,7 +966,7 @@ const game_poll = (model, msg) => ( } return false; }) - .catch(error => false) + .catch(_error => false) .then(value => { model.poll_timeout = setTimeout_(game_poll, 5e3, model, null); return value; diff --git a/src/game/m_world_store.js b/src/game/m_world_store.js index 97b7e42..5c7a3a9 100644 --- a/src/game/m_world_store.js +++ b/src/game/m_world_store.js @@ -1,7 +1,3 @@ -import { - defer, -} from '../etc/lui.js'; - import { WORLD_STORED_NOT, WORLD_STORED_SHOULD, @@ -48,21 +44,24 @@ import { } from '../etc/storage.js'; /** + last fetched remote list @type {?Array} */ export let world_list_remote = null; -let world_list_merged = null; let reload_cooldown_timeout = 0; -let syncing = false; +// just a hint so the ui selection can be kept export let world_renamed_id_old = null; export let world_renamed_id_new = 0; export const world_store_init = () => world_list_remote_load(true); export const world_store_remote_reload = () => { - if (app_state.world_list_loading) return; + if ( + app_state.world_list_loading || + app_state.world_syncing !== null + ) return; clearTimeout_(reload_cooldown_timeout); actions.state_patch({ world_list_cooldown: true, @@ -83,7 +82,7 @@ const world_list_remote_load = async initial => { world_list_remote = /** @type {!Array} */ (result); actions.state_patch({ world_list_loading: false, - worlds_merged: world_store_lists_merge(app_state.config), + ...world_store_lists_merge(app_state.config), }); } else { @@ -99,7 +98,7 @@ const world_list_remote_load = async initial => { actions.state_patch({ account: json_initial.account, world_list_loading: false, - worlds_merged: world_store_lists_merge(app_state.config), + ...world_store_lists_merge(app_state.config), }); } } @@ -156,10 +155,17 @@ export const world_store_lists_merge = config => { datify(last_change_here, false) + locale_error_conflict_4 )) { - defer(); - actions.world_prop(world_local.id, { - mod_l: world_list_item.local = last_sync, - }); + config = { + ...config, + worlds: config.worlds.map(world => ( + world.id === world_local.id + ? { + ...world, + mod_l: world_list_item.local = last_sync, + } + : world + )), + }; } else { world_list_item.remote = last_sync; @@ -177,11 +183,17 @@ export const world_store_lists_merge = config => { world_local.label + locale_warn_world_remote_missing_2 ); - debugger; - /*defer(); - actions.world_prop(world_local.id, { - mod_r: WORLD_STORED_NOT, - });*/ + config = { + ...config, + worlds: config.worlds.map(world => ( + world.id === world_local.id + ? { + ...world, + mod_r: WORLD_STORED_NOT, + } + : world + )), + }; } world_list.push({ @@ -201,31 +213,31 @@ export const world_store_lists_merge = config => { } } - return ( - world_list_merged = world_list.sort(world_list_sort) - ); + return { + config, + worlds_merged: world_list.sort(world_list_sort), + }; } /** check for worlds to be synced and do it then */ export const world_store_sync_check = async () => { - if (syncing) return; - syncing = true; + if (app_state.world_syncing !== null) return; let world_syncable; try { while ( - world_syncable = world_list_merged.find(world => + world_syncable = app_state.worlds_merged.find(world => world.local > WORLD_STORED_NOT && world.remote > WORLD_STORED_NOT && world.local !== world.remote ) ) { await world_store_sync(world_syncable); - actions.state_patch({ - world_syncing: null, - }); } + actions.state_patch({ + world_syncing: null, + }); } catch (error) { actions.state_patch({ @@ -233,7 +245,6 @@ export const world_store_sync_check = async () => { world_syncing: null, }); } - syncing = false; } /** @@ -249,10 +260,11 @@ const world_store_sync = async world => { // download? if (world.local < world.remote) { try { - const response = await fetch_(`${API_DATA}worlds/${world.hash}.json`); - const result = await response.json(); - await chunks_set(id, /** @type {!Object} */ (result)); - defer(); + await chunks_set(id, /** @type {!Object} */ ( + await response_parse( + await fetch_(`${API_DATA}worlds/${world.hash}.json`) + ) + )); actions.world_prop(id, { mod_l: world.remote, mod_r: world.remote, @@ -261,9 +273,8 @@ const world_store_sync = async world => { catch (error) { if (error.name === 'QuotaExceededError') { alert_(locale_error_storage); - defer(); - actions.world_remove(id); chunks_delete(id); + actions.world_remove(id); } else { throw Error_(locale_error_download_world + error.message); @@ -272,66 +283,68 @@ const world_store_sync = async world => { } // upload not allowed? else if (!world.writable) { - defer(); // set local modification time to remote to prevent further sync attempts actions.world_prop(id, { - mod_l: app_state.config.worlds.find(world => world.id === id).mod_r, + mod_l: world.remote, + // before it was this: app_state.config.worlds.find(world => world.id === id).mod_r, }); } // upload? else { + let id_new = id; + // can be done in background as its only the chunks id in db + let rename_promise = null; try { const data_promise = chunks_get(id); - let id_new = id; - let rename_promise = null; // world must be registered first? - if (world.remote === WORLD_STORED_SHOULD) { - const response_register = await fetch_(API + 'world', { - ...headers_json_post, - body: JSON_stringify({ - what: 'meta', - label: world.label, - }), - }); - const result_register = await response_parse(response_register); + if (id < 0) { + const result_register = await response_parse( + await fetch_(API + 'world', { + ...headers_json_post, + body: JSON_stringify({ + what: 'meta', + label: world.label, + }), + }) + ); rename_promise = chunks_rename( world_renamed_id_old = id, world_renamed_id_new = id_new = result_register.id ); - } - - const response_upload = await fetch_(API + 'world', { - ...headers_json_post, - body: JSON_stringify({ - what: 'data', - world: id_new, - data: await data_promise, - }), - }); - const result_upload = await response_parse(response_upload); - defer(); - if (world.remote === WORLD_STORED_SHOULD) { actions.world_prop(id, { id: id_new, - mod_l: result_upload.modified, - mod_r: result_upload.modified, }); - await rename_promise; } + + const result_upload = await response_parse( + await fetch_(API + 'world', { + ...headers_json_post, + body: JSON_stringify({ + what: 'data', + world: id_new, + data: await data_promise, + }), + }) + ); + if (id < 0) world_list_remote.push(result_upload); else { - actions.world_prop(id, { - mod_l: result_upload.modified, - mod_r: result_upload.modified, - }); + Object.assign( + world_list_remote.find(world => world.id === id), + result_upload + ); } + actions.world_prop(id_new, { + mod_l: result_upload.modified, + mod_r: result_upload.modified, + }); } catch (error) { - defer(); actions.world_prop(id, { mod_r: WORLD_STORED_NOT, }); throw Error_(locale_error_upload_world + error.message); } + await rename_promise; } } From 7915cb902e986126ede4323c197b095a66a0da96 Mon Sep 17 00:00:00 2001 From: L3P3 Date: Sun, 11 Jan 2026 15:20:54 +0100 Subject: [PATCH 6/8] fix: world leave while doing poll request --- src/game/m_game.js | 10 ++-------- src/game/m_world_store.js | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/game/m_game.js b/src/game/m_game.js index 6d6ba1f..d08974a 100644 --- a/src/game/m_game.js +++ b/src/game/m_game.js @@ -940,6 +940,7 @@ export const game_block_select = (model, block, secondary) => { const game_poll = (model, msg) => ( clearTimeout_(model.poll_timeout), + model.poll_timeout = setTimeout_(game_poll, 5e3, model, null), ( msg ? fetch_(API_CHAT, { @@ -950,10 +951,7 @@ const game_poll = (model, msg) => ( }) : fetch_(API_CHAT) ) - .then(res => { - if (!res.ok) return; - return res.text() - }) + .then(res => res.ok && res.text()) .then(text => { if (text) { const lines = text.split('\n').filter(Boolean); @@ -967,8 +965,4 @@ const game_poll = (model, msg) => ( return false; }) .catch(_error => false) - .then(value => { - model.poll_timeout = setTimeout_(game_poll, 5e3, model, null); - return value; - }) ) diff --git a/src/game/m_world_store.js b/src/game/m_world_store.js index 5c7a3a9..c0e783e 100644 --- a/src/game/m_world_store.js +++ b/src/game/m_world_store.js @@ -330,7 +330,7 @@ const world_store_sync = async world => { if (id < 0) world_list_remote.push(result_upload); else { Object.assign( - world_list_remote.find(world => world.id === id), + /** @type {Object} */ (world_list_remote.find(world => world.id === id)), result_upload ); } From 8dc56b80d83dc98eb1da7f10df6232dce10edf65 Mon Sep 17 00:00:00 2001 From: L3P3 Date: Sun, 11 Jan 2026 15:31:34 +0100 Subject: [PATCH 7/8] fix: delete chunks after rename --- src/etc/storage.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/etc/storage.js b/src/etc/storage.js index 861a9ac..0e0aa05 100644 --- a/src/etc/storage.js +++ b/src/etc/storage.js @@ -229,6 +229,7 @@ export const chunks_rename = indexedDB_ const cursor = request.result; if (cursor) { if (cursor.value.world === world_old) { + cursor.delete(); cursor.value.world = world_new; store.put(cursor.value); } From dd5e6fba761fde972569a362c328e3f7588b7d81 Mon Sep 17 00:00:00 2001 From: L3P3 Date: Sun, 11 Jan 2026 15:34:01 +0100 Subject: [PATCH 8/8] fix: closure compiler warning --- src/game/m_world_store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/m_world_store.js b/src/game/m_world_store.js index c0e783e..17f1610 100644 --- a/src/game/m_world_store.js +++ b/src/game/m_world_store.js @@ -330,7 +330,7 @@ const world_store_sync = async world => { if (id < 0) world_list_remote.push(result_upload); else { Object.assign( - /** @type {Object} */ (world_list_remote.find(world => world.id === id)), + /** @type {!Object} */ (world_list_remote.find(world => world.id === id)), result_upload ); }