From 89f7cfa6440169728fc1759ec169ab453cb2c4e6 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sun, 18 Aug 2024 14:39:02 +0300 Subject: [PATCH 01/16] feat: optimize build: load faster by 15% and do not duplicate three js import (tree-shake instead) --- rsbuild.config.ts | 12 +++++++----- src/index.ts | 3 +-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/rsbuild.config.ts b/rsbuild.config.ts index bb5750f7..b467ebae 100644 --- a/rsbuild.config.ts +++ b/rsbuild.config.ts @@ -54,6 +54,7 @@ export default defineConfig({ crypto: './src/shims/crypto.js', dns: './src/shims/dns.js', yggdrasil: './src/shims/yggdrasilReplacement.ts', + 'three$': 'three/src/Three.js' }, entry: { index: './src/index.ts', @@ -182,9 +183,10 @@ export default defineConfig({ ] } }, - performance: { - // bundleAnalyze: { - // analyzerMode: 'json', - // }, - }, + // performance: { + // bundleAnalyze: { + // analyzerMode: 'json', + // reportFilename: 'report.json', + // }, + // }, }) diff --git a/src/index.ts b/src/index.ts index 39626d54..3d311faa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -98,7 +98,6 @@ import { signInMessageState } from './react/SignInMessageProvider' import { updateAuthenticatedAccountData, updateLoadedServerData } from './react/ServersListProvider' import { versionToNumber } from 'prismarine-viewer/viewer/prepare/utils' import packetsPatcher from './packetsPatcher' -import blockstatesModels from 'mc-assets/dist/blockStatesModels.json' import { mainMenuState } from './react/MainMenuRenderApp' import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer' import './mobileShim' @@ -410,7 +409,7 @@ async function connect (connectOptions: ConnectOptions) { throw err } } - viewer.world.blockstatesModels = blockstatesModels + viewer.world.blockstatesModels = await import('mc-assets/dist/blockStatesModels.json') viewer.setVersion(version, options.useVersionsTextures === 'latest' ? version : options.useVersionsTextures) } From 0c99f4d5e0251467af01b2ec6ddac90d108a727f Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sun, 18 Aug 2024 14:39:14 +0300 Subject: [PATCH 02/16] feat: optimize chunks loading: do less duplicated work when chunks are received quickly --- prismarine-viewer/viewer/lib/viewer.ts | 21 ++++++++++++++++++- .../viewer/lib/worldDataEmitter.ts | 6 ++++++ .../viewer/lib/worldrendererThree.ts | 5 +++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/prismarine-viewer/viewer/lib/viewer.ts b/prismarine-viewer/viewer/lib/viewer.ts index d52f79b7..b815da6a 100644 --- a/prismarine-viewer/viewer/lib/viewer.ts +++ b/prismarine-viewer/viewer/lib/viewer.ts @@ -170,6 +170,8 @@ export class Viewer { }) } + addChunksBatchWaitTime = 200 + // todo type listen (emitter: EventEmitter) { emitter.on('entity', (e) => { @@ -180,9 +182,26 @@ export class Viewer { // this.updatePrimitive(p) }) + let currentLoadChunkBatch = null as { + timeout + data + } | null emitter.on('loadChunk', ({ x, z, chunk, worldConfig, isLightUpdate }) => { this.world.worldConfig = worldConfig - this.addColumn(x, z, chunk, isLightUpdate) + if (!currentLoadChunkBatch) { + // add a setting to use debounce instead + currentLoadChunkBatch = { + data: [], + timeout: setTimeout(() => { + for (const args of currentLoadChunkBatch!.data) { + //@ts-ignore + this.addColumn(...args) + } + currentLoadChunkBatch = null + }, this.addChunksBatchWaitTime) + } + } + currentLoadChunkBatch.data.push([x, z, chunk, isLightUpdate]) }) // todo remove and use other architecture instead so data flow is clear emitter.on('blockEntities', (blockEntities) => { diff --git a/prismarine-viewer/viewer/lib/worldDataEmitter.ts b/prismarine-viewer/viewer/lib/worldDataEmitter.ts index 9e36a89a..46fd28eb 100644 --- a/prismarine-viewer/viewer/lib/worldDataEmitter.ts +++ b/prismarine-viewer/viewer/lib/worldDataEmitter.ts @@ -150,6 +150,9 @@ export class WorldDataEmitter extends EventEmitter { } } + // debugGotChunkLatency = [] as number[] + // lastTime = 0 + async loadChunk (pos: ChunkPos, isLightUpdate = false) { const [botX, botZ] = chunkPos(this.lastPos) const dx = Math.abs(botX - Math.floor(pos.x / 16)) @@ -158,6 +161,9 @@ export class WorldDataEmitter extends EventEmitter { // eslint-disable-next-line @typescript-eslint/await-thenable -- todo allow to use async world provider but not sure if needed const column = await this.world.getColumnAt(pos['y'] ? pos as Vec3 : new Vec3(pos.x, 0, pos.z)) if (column) { + // const latency = Math.floor(performance.now() - this.lastTime) + // this.debugGotChunkLatency.push(latency) + // this.lastTime = performance.now() // todo optimize toJson data, make it clear why it is used const chunk = column.toJson() // TODO: blockEntities diff --git a/prismarine-viewer/viewer/lib/worldrendererThree.ts b/prismarine-viewer/viewer/lib/worldrendererThree.ts index 72c7d242..6c14e243 100644 --- a/prismarine-viewer/viewer/lib/worldrendererThree.ts +++ b/prismarine-viewer/viewer/lib/worldrendererThree.ts @@ -66,6 +66,7 @@ export class WorldRendererThree extends WorldRendererCommon { } } + // debugRecomputedDeletedObjects = 0 handleWorkerMessage (data: any): void { if (data.type !== 'geometry') return let object: THREE.Object3D = this.sectionObjects[data.key] @@ -78,6 +79,10 @@ export class WorldRendererThree extends WorldRendererCommon { const chunkCoords = data.key.split(',') if (!this.loadedChunks[chunkCoords[0] + ',' + chunkCoords[2]] || !data.geometry.positions.length || !this.active) return + // if (object) { + // this.debugRecomputedDeletedObjects++ + // } + // if (!this.initialChunksLoad && this.enableChunksLoadDelay) { // const newPromise = new Promise(resolve => { // if (this.droppedFpsPercentage > 0.5) { From adca2bc49425d2d942ae7c8aac9a34bc7ff910ef Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sun, 18 Aug 2024 15:16:19 +0300 Subject: [PATCH 03/16] fix lint --- prismarine-viewer/viewer/lib/viewer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prismarine-viewer/viewer/lib/viewer.ts b/prismarine-viewer/viewer/lib/viewer.ts index b815da6a..7893bccc 100644 --- a/prismarine-viewer/viewer/lib/viewer.ts +++ b/prismarine-viewer/viewer/lib/viewer.ts @@ -194,7 +194,7 @@ export class Viewer { data: [], timeout: setTimeout(() => { for (const args of currentLoadChunkBatch!.data) { - //@ts-ignore + //@ts-expect-error this.addColumn(...args) } currentLoadChunkBatch = null From 24fd4d4fc0ab57661b7cd49306d3d4395eb6acea Mon Sep 17 00:00:00 2001 From: Vitaly Date: Mon, 19 Aug 2024 14:01:13 +0300 Subject: [PATCH 04/16] feat: implement fast world loading with file descriptor & http backend! (#182) --- README.MD | 26 +++++++++++++++++++++++++- server.js | 1 + src/browserfs.ts | 38 ++++++++++++++++++++++++++++++++++++++ src/downloadAndOpenFile.ts | 13 ++++++++++++- 4 files changed, 76 insertions(+), 2 deletions(-) diff --git a/README.MD b/README.MD index dcfe3fd4..b21e8c8a 100644 --- a/README.MD +++ b/README.MD @@ -141,7 +141,31 @@ Single player specific: - `?singleplayer=1` - Create empty world on load. Nothing will be saved - `?version=` - Set the version for the singleplayer world (when used with `?singleplayer=1`) - `?noSave=true` - Disable auto save on unload / disconnect / export whenever a world is loaded. Only manual save with `/save` command will work. -- `?map=` - Load the map from ZIP. You can use any url, but it must be CORS enabled. +- `?map=` - Load the map from ZIP. You can use any url, but it must be **CORS enabled**. +- `?mapDir=` - Load the map from a file descriptor. It's recommended and the fastest way to load world but requires additional setup. The file must be in the following format: + +```json +{ + "baseUrl": "", + "index": { + "level.dat": null, + "region": { + "r.-1.-1.mca": null, + "r.-1.0.mca": null, + "r.0.-1.mca": null, + "r.0.0.mca": null, + } + } +} +``` + +Note that `mapDir` also accepts base64 encoded JSON like so: +`?mapDir=data:application/json;base64,...` where `...` is the base64 encoded JSON of the index file. +In this case you must use `?mapDirBaseUrl` to specify the base URL to fetch the files from index. + +- `?mapDirBaseUrl` - See above. + + General: diff --git a/server.js b/server.js index 7adae4bb..dd1ccaa1 100644 --- a/server.js +++ b/server.js @@ -17,6 +17,7 @@ const app = express() const isProd = process.argv.includes('--prod') app.use(compression()) +// app.use(cors()) app.use(netApi({ allowOrigin: '*' })) if (!isProd) { app.use('/sounds', express.static(path.join(__dirname, './generated/sounds/'))) diff --git a/src/browserfs.ts b/src/browserfs.ts index 16691986..0c4c7664 100644 --- a/src/browserfs.ts +++ b/src/browserfs.ts @@ -433,6 +433,44 @@ export const copyFilesAsync = async (pathSrc: string, pathDest: string, fileCopi })) } +export const openWorldFromHttpDir = async (fileDescriptorUrl: string/* | undefined */, baseUrl = fileDescriptorUrl.split('/').slice(0, -1).join('/')) => { + // todo try go guess mode + let index + const file = await fetch(fileDescriptorUrl).then(async a => a.json()) + if (file.baseUrl) { + baseUrl = new URL(file.baseUrl, baseUrl).toString() + index = file.index + } else { + index = file + } + if (!index) throw new Error(`The provided mapDir file is not valid descriptor file! ${fileDescriptorUrl}`) + await new Promise(async resolve => { + browserfs.configure({ + fs: 'MountableFileSystem', + options: { + ...defaultMountablePoints, + '/world': { + fs: 'HTTPRequest', + options: { + index, + baseUrl + } + } + }, + }, (e) => { + if (e) throw e + resolve() + }) + }) + + fsState.saveLoaded = false + fsState.isReadonly = true + fsState.syncFs = false + fsState.inMemorySave = false + + await loadSave() +} + // todo rename method const openWorldZipInner = async (file: File | ArrayBuffer, name = file['name']) => { await new Promise(async resolve => { diff --git a/src/downloadAndOpenFile.ts b/src/downloadAndOpenFile.ts index 7ae8547f..49fc5c35 100644 --- a/src/downloadAndOpenFile.ts +++ b/src/downloadAndOpenFile.ts @@ -1,5 +1,5 @@ import prettyBytes from 'pretty-bytes' -import { openWorldZip } from './browserfs' +import { openWorldFromHttpDir, openWorldZip } from './browserfs' import { getResourcePackNames, installTexturePack, resourcePackState, updateTexturePackInstalledState } from './resourcePack' import { setLoadingScreenStatus } from './utils' @@ -9,6 +9,17 @@ export const getFixedFilesize = (bytes: number) => { const inner = async () => { const qs = new URLSearchParams(window.location.search) + const mapUrlDir = qs.get('mapDir') + const mapUrlDirGuess = qs.get('mapDirGuess') + const mapUrlDirBaseUrl = qs.get('mapDirBaseUrl') + if (mapUrlDir) { + await openWorldFromHttpDir(mapUrlDir, mapUrlDirBaseUrl ?? undefined) + return true + } + if (mapUrlDirGuess) { + // await openWorldFromHttpDir(undefined, mapUrlDirGuess) + return true + } let mapUrl = qs.get('map') const texturepack = qs.get('texturepack') // fixme From 01c82e3c745c5b71b6487814ab692cc29e2e2ed9 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Mon, 19 Aug 2024 14:35:51 +0300 Subject: [PATCH 05/16] minor infra changes for app links --- .github/workflows/next-deploy.yml | 2 +- .github/workflows/publish.yml | 2 +- src/react/DiscordButton.tsx | 2 +- src/react/MainMenu.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/next-deploy.yml b/.github/workflows/next-deploy.yml index b3e7c1d2..f6915839 100644 --- a/.github/workflows/next-deploy.yml +++ b/.github/workflows/next-deploy.yml @@ -3,7 +3,7 @@ env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} ALIASES: ${{ vars.ALIASES }} - MAIN_MENU_LINKS: ${{ secrets.MAIN_MENU_LINKS }} + MAIN_MENU_LINKS: ${{ vars.MAIN_MENU_LINKS }} on: push: branches: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index cc06bfb0..9472118b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,7 +2,7 @@ name: Release env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} - MAIN_MENU_LINKS: ${{ secrets.MAIN_MENU_LINKS }} + MAIN_MENU_LINKS: ${{ vars.MAIN_MENU_LINKS }} on: push: branches: [release] diff --git a/src/react/DiscordButton.tsx b/src/react/DiscordButton.tsx index 0e3a22b5..99208c45 100644 --- a/src/react/DiscordButton.tsx +++ b/src/react/DiscordButton.tsx @@ -8,7 +8,7 @@ export const DiscordButton = () => { const links: DropdownButtonItem[] = [ { text: 'Support Official Server (mcraft.fun)', - clickHandler: () => openURL('https://discord.gg/JCPnD4Qh') + clickHandler: () => openURL('https://discord.gg/xzGRhxtRUt') }, { text: 'Community Server (PrismarineJS)', diff --git a/src/react/MainMenu.tsx b/src/react/MainMenu.tsx index 231e29fb..45bc4296 100644 --- a/src/react/MainMenu.tsx +++ b/src/react/MainMenu.tsx @@ -40,7 +40,7 @@ export default ({ }: Props) => { if (!bottomRightLinks?.trim()) bottomRightLinks = undefined // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - const linksParsed = bottomRightLinks?.split(';').map(l => { + const linksParsed = bottomRightLinks?.split(/;|\n/g).map(l => { const parts = l.split(':') return [parts[0], parts.slice(1).join(':')] }) as Array<[string, string]> | undefined From 04d79c16be6cdd77e57b280a21e012722bca2f80 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Wed, 21 Aug 2024 00:25:35 +0300 Subject: [PATCH 06/16] feat: minimize lags when moving between chunks (lazily unload chunks (#179) + a setting to control that --- prismarine-viewer/viewer/lib/worldDataEmitter.ts | 5 +++-- src/index.ts | 3 ++- src/optionsGuiScheme.tsx | 5 +++++ src/optionsStorage.ts | 1 + src/watchOptions.ts | 11 +++++++++++ 5 files changed, 22 insertions(+), 3 deletions(-) diff --git a/prismarine-viewer/viewer/lib/worldDataEmitter.ts b/prismarine-viewer/viewer/lib/worldDataEmitter.ts index 46fd28eb..ea956b81 100644 --- a/prismarine-viewer/viewer/lib/worldDataEmitter.ts +++ b/prismarine-viewer/viewer/lib/worldDataEmitter.ts @@ -19,6 +19,7 @@ export class WorldDataEmitter extends EventEmitter { private readonly lastPos: Vec3 private eventListeners: Record = {} private readonly emitter: WorldDataEmitter + keepChunksDistance = 0 constructor (public world: typeof __type_bot['world'], public viewDistance: number, position: Vec3 = new Vec3(0, 0, 0)) { super() @@ -197,14 +198,14 @@ export class WorldDataEmitter extends EventEmitter { const [botX, botZ] = chunkPos(pos) if (lastX !== botX || lastZ !== botZ || force) { this.emitter.emit('chunkPosUpdate', { pos }) - const newView = new ViewRect(botX, botZ, this.viewDistance) + const newViewToUnload = new ViewRect(botX, botZ, this.viewDistance + this.keepChunksDistance) const chunksToUnload: Vec3[] = [] for (const coords of Object.keys(this.loadedChunks)) { const x = parseInt(coords.split(',')[0], 10) const z = parseInt(coords.split(',')[1], 10) const p = new Vec3(x, 0, z) const [chunkX, chunkZ] = chunkPos(p) - if (!newView.contains(chunkX, chunkZ)) { + if (!newViewToUnload.contains(chunkX, chunkZ)) { chunksToUnload.push(p) } } diff --git a/src/index.ts b/src/index.ts index 3d311faa..f5a12743 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,7 +24,7 @@ import './reactUi' import { contro, onBotCreate } from './controls' import './dragndrop' import { possiblyCleanHandle, resetStateAfterDisconnect } from './browserfs' -import { watchOptionsAfterViewerInit } from './watchOptions' +import { watchOptionsAfterViewerInit, watchOptionsAfterWorldViewInit } from './watchOptions' import downloadAndOpenFile from './downloadAndOpenFile' import fs from 'fs' @@ -689,6 +689,7 @@ async function connect (connectOptions: ConnectOptions) { const center = bot.entity.position const worldView = window.worldView = new WorldDataEmitter(bot.world, renderDistance, center) + watchOptionsAfterWorldViewInit() bot.on('physicsTick', () => updateCursor()) diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx index bcf3054f..644ec66c 100644 --- a/src/optionsGuiScheme.tsx +++ b/src/optionsGuiScheme.tsx @@ -83,6 +83,11 @@ export const guiOptionsScheme: { }, starfieldRendering: {}, renderEntities: {}, + keepChunksDistance: { + max: 5, + unit: '', + tooltip: 'Additional distance to keep the chunks loading before unloading them by marking them as too far', + }, }, ], main: [ diff --git a/src/optionsStorage.ts b/src/optionsStorage.ts index 990dd31e..cac8c9f8 100644 --- a/src/optionsStorage.ts +++ b/src/optionsStorage.ts @@ -7,6 +7,7 @@ import { omitObj } from '@zardoy/utils' const defaultOptions = { renderDistance: 3, + keepChunksDistance: 1, multiplayerRenderDistance: 3, closeConfirmation: true, autoFullScreen: false, diff --git a/src/watchOptions.ts b/src/watchOptions.ts index e379cb50..9deb46a5 100644 --- a/src/watchOptions.ts +++ b/src/watchOptions.ts @@ -63,3 +63,14 @@ export const watchOptionsAfterViewerInit = () => { viewer.world.starField.enabled = o.starfieldRendering }) } + +let viewWatched = false +export const watchOptionsAfterWorldViewInit = () => { + worldView!.keepChunksDistance = options.keepChunksDistance + if (viewWatched) return + viewWatched = true + watchValue(options, o => { + if (!worldView) return + worldView.keepChunksDistance = o.keepChunksDistance + }) +} From 372059f0bebda806bfb1c73f4af7e98edc4c5fc0 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Thu, 22 Aug 2024 15:47:29 +0300 Subject: [PATCH 07/16] add highest block implementation for a few other PRs, up server --- package.json | 2 +- pnpm-lock.yaml | 10 +++---- prismarine-viewer/viewer/lib/mesher/models.ts | 16 ++++++++++-- .../viewer/lib/worldrendererCommon.ts | 26 +++++++++++++++++-- src/react/KeybindingsScreen.module.css | 1 + 5 files changed, 45 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 7b27333e..f8eb87eb 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "esbuild-plugin-polyfill-node": "^0.3.0", "express": "^4.18.2", "filesize": "^10.0.12", - "flying-squid": "npm:@zardoy/flying-squid@^0.0.34", + "flying-squid": "npm:@zardoy/flying-squid@^0.0.35", "fs-extra": "^11.1.1", "google-drive-browserfs": "github:zardoy/browserfs#google-drive", "jszip": "^3.10.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 486eebe6..4cab8b08 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -117,8 +117,8 @@ importers: specifier: ^10.0.12 version: 10.0.12 flying-squid: - specifier: npm:@zardoy/flying-squid@^0.0.34 - version: '@zardoy/flying-squid@0.0.34(encoding@0.1.13)' + specifier: npm:@zardoy/flying-squid@^0.0.35 + version: '@zardoy/flying-squid@0.0.35(encoding@0.1.13)' fs-extra: specifier: ^11.1.1 version: 11.1.1 @@ -3386,8 +3386,8 @@ packages: resolution: {integrity: sha512-6xm38yGVIa6mKm/DUCF2zFFJhERh/QWp1ufm4cNUvxsONBmfPg8uZ9pZBdOmF6qFGr/HlT6ABBkCSx/dlEtvWg==} engines: {node: '>=12 <14 || 14.2 - 14.9 || >14.10.0'} - '@zardoy/flying-squid@0.0.34': - resolution: {integrity: sha512-1q9AE4GfmRQhKnSJ3QJtLZIznjJ/IcvwjjKKBS/LrxzaN+qsa3RI2H68OOULj5r/tiGU9DQNncW3CpMlezH6gA==} + '@zardoy/flying-squid@0.0.35': + resolution: {integrity: sha512-6cZdDi7yaqxh6KbOPhDueipcr9DBgJ3mJY+/QwAjaSzhP//5n1BLjyVGlx2Ncs/6Vns2grTOmeuDhJjMbVgjQg==} engines: {node: '>=8'} hasBin: true @@ -12923,7 +12923,7 @@ snapshots: '@types/emscripten': 1.39.8 tslib: 1.14.1 - '@zardoy/flying-squid@0.0.34(encoding@0.1.13)': + '@zardoy/flying-squid@0.0.35(encoding@0.1.13)': dependencies: '@tootallnate/once': 2.0.0 change-case: 4.1.2 diff --git a/prismarine-viewer/viewer/lib/mesher/models.ts b/prismarine-viewer/viewer/lib/mesher/models.ts index f20fd1f2..c4249ac4 100644 --- a/prismarine-viewer/viewer/lib/mesher/models.ts +++ b/prismarine-viewer/viewer/lib/mesher/models.ts @@ -354,6 +354,8 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO: } } +const makeLooseObj = (obj: Record) => obj + const invisibleBlocks = new Set(['air', 'cave_air', 'void_air', 'barrier']) const isBlockWaterlogged = (block: Block) => block.getProperties().waterlogged === true || block.getProperties().waterlogged === 'true' @@ -363,7 +365,7 @@ let erroredBlockModel: BlockModelPartsResolved export function getSectionGeometry (sx, sy, sz, world: World) { let delayedRender = [] as Array<() => void> - const attr = { + const attr = makeLooseObj({ sx: sx + 8, sy: sy + 8, sz: sz + 8, @@ -379,14 +381,24 @@ export function getSectionGeometry (sx, sy, sz, world: World) { tiles: {}, // todo this can be removed here signs: {}, + highestBlocks: {}, hadErrors: false - } as Record + } as Record) const cursor = new Vec3(0, 0, 0) for (cursor.y = sy; cursor.y < sy + 16; cursor.y++) { for (cursor.z = sz; cursor.z < sz + 16; cursor.z++) { for (cursor.x = sx; cursor.x < sx + 16; cursor.x++) { const block = world.getBlock(cursor)! + if (!invisibleBlocks.has(block.name)) { + const highest = attr.highestBlocks[`${cursor.x},${cursor.z}`] + if (!highest || highest.y < cursor.y) { + attr.highestBlocks[`${cursor.x},${cursor.z}`] = { + y: cursor.y, + name: block.name + } + } + } if (invisibleBlocks.has(block.name)) continue if (block.name.includes('_sign') || block.name === 'sign') { const key = `${cursor.x},${cursor.y},${cursor.z}` diff --git a/prismarine-viewer/viewer/lib/worldrendererCommon.ts b/prismarine-viewer/viewer/lib/worldrendererCommon.ts index fbc6db46..204ad65f 100644 --- a/prismarine-viewer/viewer/lib/worldrendererCommon.ts +++ b/prismarine-viewer/viewer/lib/worldrendererCommon.ts @@ -75,6 +75,7 @@ export abstract class WorldRendererCommon handleResize = () => { } mesherConfig = defaultMesherConfig camera: THREE.PerspectiveCamera + highestBlocks: Record = {} blockstatesModels: any customBlockStates: Record | undefined customModels: Record | undefined @@ -109,7 +110,15 @@ export abstract class WorldRendererCommon const handleMessage = (data) => { if (!this.active) return this.handleWorkerMessage(data) - if (data.type === 'sectionFinished') { + if (data.type === 'geometry') { + for (const key in data.geometry.highestBlocks) { + const highest = data.geometry.highestBlocks[key] + if (!this.highestBlocks[key] || this.highestBlocks[key].y < highest.y) { + this.highestBlocks[key] = highest + } + } + } + if (data.type === 'sectionFinished') { // on after load & unload section if (!this.sectionsOutstanding.get(data.key)) throw new Error(`sectionFinished event for non-outstanding section ${data.key}`) this.sectionsOutstanding.set(data.key, this.sectionsOutstanding.get(data.key)! - 1) if (this.sectionsOutstanding.get(data.key) === 0) this.sectionsOutstanding.delete(data.key) @@ -302,6 +311,19 @@ export abstract class WorldRendererCommon } this.allChunksFinished = Object.keys(this.finishedChunks).length === this.chunksLength delete this.finishedChunks[`${x},${z}`] + for (let y = this.worldConfig.minY; y < this.worldConfig.worldHeight; y += 16) { + this.setSectionDirty(new Vec3(x, y, z), false) + } + // remove from highestBlocks + const startX = Math.floor(x / 16) * 16 + const startZ = Math.floor(z / 16) * 16 + const endX = Math.ceil((x + 1) / 16) * 16 + const endZ = Math.ceil((z + 1) / 16) * 16 + for (let x = startX; x < endX; x += 16) { + for (let z = startZ; z < endZ; z += 16) { + delete this.highestBlocks[`${x},${z}`] + } + } } setBlockStateId (pos: Vec3, stateId: number) { @@ -320,7 +342,7 @@ export abstract class WorldRendererCommon queueAwaited = false messagesQueue = {} as { [workerIndex: string]: any[] } - setSectionDirty (pos: Vec3, value = true) { + setSectionDirty (pos: Vec3, value = true) { // value false is used for unloading chunks if (this.viewDistance === -1) throw new Error('viewDistance not set') this.allChunksFinished = false const distance = this.getDistance(pos) diff --git a/src/react/KeybindingsScreen.module.css b/src/react/KeybindingsScreen.module.css index e8a9f69b..102974ce 100644 --- a/src/react/KeybindingsScreen.module.css +++ b/src/react/KeybindingsScreen.module.css @@ -49,6 +49,7 @@ .undo-keyboard, .undo-gamepad { aspect-ratio: 1; + min-width: 20px; } .button { From 42e53c8efcfcb1a1f0d8a2f037b694b06a580088 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sat, 24 Aug 2024 04:34:35 +0300 Subject: [PATCH 08/16] fix: correctly display versions list in Select component fix: was unable to create new worlds by pressing Enter --- src/react/CreateWorld.tsx | 2 +- src/react/Select.stories.tsx | 2 +- src/react/Select.tsx | 12 ++++++------ src/react/SelectGameVersion.tsx | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/react/CreateWorld.tsx b/src/react/CreateWorld.tsx index 698d5d09..0ff34a39 100644 --- a/src/react/CreateWorld.tsx +++ b/src/react/CreateWorld.tsx @@ -81,7 +81,7 @@ export default ({ cancelClick, createClick, customizeClick, versions, defaultVer -
Note: store important saves in folders on the drive!
+
Note: save important worlds in folders on your hard drive!
{quota}
} diff --git a/src/react/Select.stories.tsx b/src/react/Select.stories.tsx index 19ada27a..f50b8c6a 100644 --- a/src/react/Select.stories.tsx +++ b/src/react/Select.stories.tsx @@ -14,7 +14,7 @@ export const Primary: Story = { args: { initialOptions: [{ value: '1', label: 'option 1' }, { value: '2', label: 'option 2' }, { value: '3', label: 'option 3' },], updateOptions (options) {}, - processInput (input) { + getCssOnInput (input) { console.log('input:', input) if (input === 'option 3') return { border: '1px solid yellow' } as CSSProperties }, diff --git a/src/react/Select.tsx b/src/react/Select.tsx index f4285330..7fe8aaea 100644 --- a/src/react/Select.tsx +++ b/src/react/Select.tsx @@ -13,7 +13,7 @@ export interface OptionStorage { interface Props { initialOptions: OptionStorage[] updateOptions: (options: string) => void - processInput?: (input: string) => CSSProperties | undefined + getCssOnInput?: (input: string) => CSSProperties | undefined processOption?: (option: string) => string onValueChange?: (newVal: string) => void defaultValue?: { value: string, label: string } @@ -27,7 +27,7 @@ interface Props { export default ({ initialOptions, updateOptions, - processInput, + getCssOnInput, onValueChange, defaultValue, containerStyle, @@ -40,7 +40,6 @@ export default ({ return { setIsFirstClick(false) @@ -72,6 +71,7 @@ export default ({ onMenuOpen={() => { setIsFirstClick(true) }} + menuPortalTarget={document.body} classNames={{ control (state) { return styles.container @@ -84,7 +84,8 @@ export default ({ } }} styles={{ - container (base, state) { return { ...base, position: 'relative' } }, + menuPortal (base, state) { return { ...base, zIndex: 10, transform: 'scale(var(--guiScale))', transformOrigin: 'top left' } }, + container (base, state) { return { ...base, position: 'relative', zIndex: 10 } }, control (base, state) { return { ...containerStyle, ...inputStyle } }, menu (base, state) { return { position: 'absolute', zIndex: 10 } }, option (base, state) { @@ -106,4 +107,3 @@ export default ({ }} /> } - diff --git a/src/react/SelectGameVersion.tsx b/src/react/SelectGameVersion.tsx index 4944faa5..ec8c9726 100644 --- a/src/react/SelectGameVersion.tsx +++ b/src/react/SelectGameVersion.tsx @@ -23,7 +23,7 @@ export default ( }} onValueChange={onChange} containerStyle={containerStyle ?? { width: '190px' }} - processInput={(value) => { + getCssOnInput={(value) => { if (!versions || !value) return {} const parsedsupportedVersions = versions.map(x => x.value.split('.').map(Number)) const parsedValue = value.split('.').map(Number) From 69e06162408e7988d6d9f3fdf8d71d8f305a0d80 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sat, 24 Aug 2024 04:54:20 +0300 Subject: [PATCH 09/16] fix(regression): select version was not visible in server options --- src/react/AddServerOrConnect.tsx | 7 +++---- src/react/Select.tsx | 9 ++++----- src/react/SelectGameVersion.tsx | 5 +---- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/react/AddServerOrConnect.tsx b/src/react/AddServerOrConnect.tsx index a933a1d7..06f4fcd7 100644 --- a/src/react/AddServerOrConnect.tsx +++ b/src/react/AddServerOrConnect.tsx @@ -101,14 +101,13 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ }}> { return { value: v, label: v } }) ?? []} onChange={(value) => { setVersionOverride(value) }} - // inputProps={{ - // placeholder: 'Optional, but recommended to specify', - // disabled: lockConnect && qsParamVersion !== null - // }} + placeholder="Optional, but recommended to specify" + disabled={lockConnect && qsParamVersion !== null} /> diff --git a/src/react/Select.tsx b/src/react/Select.tsx index 7fe8aaea..8d79fe54 100644 --- a/src/react/Select.tsx +++ b/src/react/Select.tsx @@ -14,14 +14,11 @@ interface Props { initialOptions: OptionStorage[] updateOptions: (options: string) => void getCssOnInput?: (input: string) => CSSProperties | undefined - processOption?: (option: string) => string onValueChange?: (newVal: string) => void defaultValue?: { value: string, label: string } - iconInput?: string placeholder?: string - iconOption?: string containerStyle?: CSSProperties - inputProps?: React.ComponentProps + disabled?: boolean } export default ({ @@ -31,7 +28,8 @@ export default ({ onValueChange, defaultValue, containerStyle, - placeholder + placeholder, + disabled }: Props) => { const [inputValue, setInputValue] = useState(defaultValue?.label ?? '') const [currValue, setCurrValue] = useState(defaultValue?.label ?? '') @@ -48,6 +46,7 @@ export default ({ formatCreateLabel={(value) => { return 'Use "' + value + '"' }} + isDisabled={disabled} placeholder={placeholder ?? ''} onChange={(e, action) => { console.log('value:', e?.value) diff --git a/src/react/SelectGameVersion.tsx b/src/react/SelectGameVersion.tsx index ec8c9726..8188b827 100644 --- a/src/react/SelectGameVersion.tsx +++ b/src/react/SelectGameVersion.tsx @@ -1,6 +1,5 @@ import React, { CSSProperties } from 'react' import Select from './Select' -import Input from './Input' type Version = { value: string, label: string } @@ -9,11 +8,9 @@ export default ( { versions: Version[], selected?: Version, - inputProps?: React.ComponentProps, onChange?: (newValue: string) => void, updateOptions?: (newSel: string) => void, - containerStyle?: CSSProperties - } + } & Pick, 'containerStyle' | 'placeholder' | 'disabled'> ) => { return Date: Sun, 25 Aug 2024 23:43:10 +0300 Subject: [PATCH 12/16] fix: make entities movements smoother --- prismarine-viewer/viewer/lib/entities.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prismarine-viewer/viewer/lib/entities.js b/prismarine-viewer/viewer/lib/entities.js index 8d8630ac..57a45a44 100644 --- a/prismarine-viewer/viewer/lib/entities.js +++ b/prismarine-viewer/viewer/lib/entities.js @@ -15,7 +15,7 @@ import { WalkingGeneralSwing } from './entity/animations' import externalTexturesJson from './entity/externalTextures.json' import { disposeObject } from './threeJsUtils' -export const TWEEN_DURATION = 50 // todo should be 100 +export const TWEEN_DURATION = 120 /** * @param {string} username From de3907aefedbb9516283bd1b0dcdee795e17b709 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sun, 25 Aug 2024 23:53:07 +0300 Subject: [PATCH 13/16] add more context to item render error --- src/inventoryWindows.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/inventoryWindows.ts b/src/inventoryWindows.ts index 6ef8ebf7..247fad67 100644 --- a/src/inventoryWindows.ts +++ b/src/inventoryWindows.ts @@ -166,7 +166,7 @@ const renderSlot = (slot: RenderSlot, skipBlock = false): { itemTexture = itemsRenderer.getItemTexture(itemName) ?? itemsRenderer.getItemTexture('item/missing_texture')! } catch (err) { itemTexture = itemsRenderer.getItemTexture('block/errored')! - inGameError(err) + inGameError(`Failed to render item ${itemName} on ${bot.version} (resourcepack: ${options.enabledResourcepack}): ${err.message}`) } if ('type' in itemTexture) { // is item From 34a6f1d9c35d28786d57519069fdc73b14ec2704 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Mon, 26 Aug 2024 00:41:23 +0300 Subject: [PATCH 14/16] fix: fix critical bug which was resulting in incorrect modals (and whole app) state in some extremely rare cases --- src/globalState.ts | 36 +++++++++++------------------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/src/globalState.ts b/src/globalState.ts index 3e6c06e8..d4366399 100644 --- a/src/globalState.ts +++ b/src/globalState.ts @@ -35,24 +35,11 @@ subscribe(activeModalStack, () => { } }) -export const customDisplayManageKeyword = 'custom' - -const defaultModalActions = { - show (modal: Modal) { - if (modal.elem) modal.elem.style.display = 'block' - }, - hide (modal: Modal) { - if (modal.elem) modal.elem.style.display = 'none' - } -} - /** * @returns true if operation was successful */ const showModalInner = (modal: Modal) => { const cancel = modal.elem?.show?.() - if (cancel && cancel !== customDisplayManageKeyword) return false - if (cancel !== 'custom') defaultModalActions.show(modal) return true } @@ -60,7 +47,6 @@ export const showModal = (elem: /* (HTMLElement & Record) | */{ re const resolved = elem const curModal = activeModalStack.at(-1) if (/* elem === curModal?.elem || */(elem.reactType && elem.reactType === curModal?.reactType) || !showModalInner(resolved)) return - if (curModal) defaultModalActions.hide(curModal) activeModalStack.push(resolved) } @@ -71,21 +57,21 @@ export const showModal = (elem: /* (HTMLElement & Record) | */{ re export const hideModal = (modal = activeModalStack.at(-1), data: any = undefined, options: { force?: boolean; restorePrevious?: boolean } = {}) => { const { force = false, restorePrevious = true } = options if (!modal) return - let cancel - if (modal.elem) { - cancel = modal.elem.hide?.(data) - } else if (modal.reactType) { - cancel = notHideableModalsWithoutForce.has(modal.reactType) ? !force : undefined - } - if (force && cancel !== customDisplayManageKeyword) { + let cancel = notHideableModalsWithoutForce.has(modal.reactType) ? !force : undefined + if (force) { cancel = undefined } - if (!cancel || cancel === customDisplayManageKeyword) { - if (cancel !== customDisplayManageKeyword) defaultModalActions.hide(modal) - activeModalStack.pop() + if (!cancel) { + let lastModal = activeModalStack.at(-1) + for (let i = activeModalStack.length - 1; i >= 0; i--) { + if (activeModalStack[i].reactType === modal.reactType) { + activeModalStack.splice(i, 1) + break + } + } const newModal = activeModalStack.at(-1) - if (newModal && restorePrevious) { + if (newModal && lastModal !== newModal && restorePrevious) { // would be great to ignore cancel I guess? showModalInner(newModal) } From ffc9a0c458c33c54f94e9b2bbac47ca56a1d0cb9 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Mon, 26 Aug 2024 03:11:23 +0300 Subject: [PATCH 15/16] fix: update "save remote world to your device" function to support new HTTP backend --- src/browserfs.ts | 47 +++++++++++++++++++++++++++++++++++-- src/globalState.ts | 2 +- src/loadSave.ts | 1 + src/react/PauseScreen.tsx | 49 ++++++++++++++++++++++++++++++++++----- 4 files changed, 90 insertions(+), 9 deletions(-) diff --git a/src/browserfs.ts b/src/browserfs.ts index 0c4c7664..82d24058 100644 --- a/src/browserfs.ts +++ b/src/browserfs.ts @@ -233,6 +233,7 @@ export const mountGoogleDriveFolder = async (readonly: boolean, rootId: string) fsState.isReadonly = readonly fsState.syncFs = false fsState.inMemorySave = false + fsState.remoteBackend = true return true } @@ -313,6 +314,7 @@ export const openWorldDirectory = async (dragndropHandle?: FileSystemDirectoryHa fsState.isReadonly = !writeAccess fsState.syncFs = false fsState.inMemorySave = false + fsState.remoteBackend = false await loadSave() } @@ -352,7 +354,33 @@ export const possiblyCleanHandle = (callback = () => { }) => { } } -export const copyFilesAsyncWithProgress = async (pathSrc: string, pathDest: string, throwRootNotExist = true) => { +const readdirSafe = async (path: string) => { + try { + return await fs.promises.readdir(path) + } catch (err) { + return null + } +} + +export const collectFilesToCopy = async (basePath: string, safe = false): Promise => { + const result: string[] = [] + const countFiles = async (relPath: string) => { + const resolvedPath = join(basePath, relPath) + const files = relPath === '.' && !safe ? await fs.promises.readdir(resolvedPath) : await readdirSafe(resolvedPath) + if (!files) return null + await Promise.all(files.map(async file => { + const res = await countFiles(join(relPath, file)) + if (res === null) { + // is file + result.push(join(relPath, file)) + } + })) + } + await countFiles('.') + return result +} + +export const copyFilesAsyncWithProgress = async (pathSrc: string, pathDest: string, throwRootNotExist = true, addMsg = '') => { const stat = await existsViaStats(pathSrc) if (!stat) { if (throwRootNotExist) throw new Error(`Cannot copy. Source directory ${pathSrc} does not exist`) @@ -387,7 +415,7 @@ export const copyFilesAsyncWithProgress = async (pathSrc: string, pathDest: stri let copied = 0 await copyFilesAsync(pathSrc, pathDest, (name) => { copied++ - setLoadingScreenStatus(`Copying files (${copied}/${filesCount}): ${name}`) + setLoadingScreenStatus(`Copying files${addMsg} (${copied}/${filesCount}): ${name}`) }) } finally { setLoadingScreenStatus(undefined) @@ -402,6 +430,19 @@ export const existsViaStats = async (path: string) => { } } +export const fileExistsAsyncOptimized = async (path: string) => { + try { + await fs.promises.readdir(path) + } catch (err) { + if (err.code === 'ENOTDIR') return true + // eslint-disable-next-line sonarjs/prefer-single-boolean-return + if (err.code === 'ENOENT') return false + // throw err + return false + } + return true +} + export const copyFilesAsync = async (pathSrc: string, pathDest: string, fileCopied?: (name) => void) => { // query: can't use fs.copy! use fs.promises.writeFile and readFile const files = await fs.promises.readdir(pathSrc) @@ -467,6 +508,7 @@ export const openWorldFromHttpDir = async (fileDescriptorUrl: string/* | undefi fsState.isReadonly = true fsState.syncFs = false fsState.inMemorySave = false + fsState.remoteBackend = true await loadSave() } @@ -497,6 +539,7 @@ const openWorldZipInner = async (file: File | ArrayBuffer, name = file['name']) fsState.isReadonly = true fsState.syncFs = true fsState.inMemorySave = false + fsState.remoteBackend = false if (fs.existsSync('/world/level.dat')) { await loadSave() diff --git a/src/globalState.ts b/src/globalState.ts index d4366399..b0a447f2 100644 --- a/src/globalState.ts +++ b/src/globalState.ts @@ -63,7 +63,7 @@ export const hideModal = (modal = activeModalStack.at(-1), data: any = undefined } if (!cancel) { - let lastModal = activeModalStack.at(-1) + const lastModal = activeModalStack.at(-1) for (let i = activeModalStack.length - 1; i >= 0; i--) { if (activeModalStack[i].reactType === modal.reactType) { activeModalStack.splice(i, 1) diff --git a/src/loadSave.ts b/src/loadSave.ts index 48054094..6c7da6bb 100644 --- a/src/loadSave.ts +++ b/src/loadSave.ts @@ -21,6 +21,7 @@ export const fsState = proxy({ saveLoaded: false, openReadOperations: 0, openWriteOperations: 0, + remoteBackend: false }) const PROPOSE_BACKUP = true diff --git a/src/react/PauseScreen.tsx b/src/react/PauseScreen.tsx index 560412de..96b00648 100644 --- a/src/react/PauseScreen.tsx +++ b/src/react/PauseScreen.tsx @@ -1,4 +1,5 @@ import { join } from 'path' +import fs from 'fs' import { useEffect } from 'react' import { useSnapshot } from 'valtio' import { usedServerPathsV1 } from 'flying-squid/dist/lib/modules/world' @@ -14,7 +15,7 @@ import { fsState } from '../loadSave' import { disconnect } from '../flyingSquidUtils' import { pointerLock, setLoadingScreenStatus } from '../utils' import { closeWan, openToWanAndCopyJoinLink, getJoinLink } from '../localServerMultiplayer' -import { copyFilesAsyncWithProgress, mkdirRecursive, uniqueFileNameFromWorldName } from '../browserfs' +import { collectFilesToCopy, fileExistsAsyncOptimized, mkdirRecursive, uniqueFileNameFromWorldName } from '../browserfs' import { useIsModalActive } from './utilsApp' import { showOptionsModal } from './SelectOption' import Button from './Button' @@ -29,12 +30,44 @@ export const saveToBrowserMemory = async () => { const { worldFolder } = localServer.options const saveRootPath = await uniqueFileNameFromWorldName(worldFolder.split('/').pop(), `/data/worlds`) await mkdirRecursive(saveRootPath) - for (const copyPath of [...usedServerPathsV1, 'icon.png']) { - const srcPath = join(worldFolder, copyPath) - const savePath = join(saveRootPath, copyPath) - // eslint-disable-next-line no-await-in-loop - await copyFilesAsyncWithProgress(srcPath, savePath, false) + const allRootPaths = [...usedServerPathsV1] + const allFilesToCopy = [] as string[] + for (const dirBase of allRootPaths) { + if (dirBase.includes('.') && await fileExistsAsyncOptimized(join(worldFolder, dirBase))) { + allFilesToCopy.push(dirBase) + continue + } + let res = await collectFilesToCopy(join(worldFolder, dirBase), true) + if (dirBase === 'region') { + res = res.filter(x => x.endsWith('.mca')) + } + allFilesToCopy.push(...res.map(x => join(dirBase, x))) } + const pathsSplit = allFilesToCopy.reduce((acc, cur, i) => { + if (i % 15 === 0) { + acc.push([]) + } + acc.at(-1)!.push(cur) + return acc + // eslint-disable-next-line @typescript-eslint/prefer-reduce-type-parameter + }, [] as string[][]) + let copied = 0 + const upProgress = () => { + copied++ + const action = fsState.remoteBackend ? 'Downloading & copying' : 'Copying' + setLoadingScreenStatus(`${action} files (${copied}/${allFilesToCopy.length})`) + } + for (const copyPaths of pathsSplit) { + // eslint-disable-next-line no-await-in-loop + await Promise.all(copyPaths.map(async (copyPath) => { + const srcPath = join(worldFolder, copyPath) + const savePath = join(saveRootPath, copyPath) + await mkdirRecursive(savePath) + await fs.promises.writeFile(savePath, await fs.promises.readFile(srcPath)) + upProgress() + })) + } + return saveRootPath } catch (err) { void showOptionsModal(`Error while saving the world: ${err.message}`, []) @@ -101,6 +134,10 @@ export default () => { const action = await showOptionsModal('World actions...', ['Save to browser memory']) if (action === 'Save to browser memory') { await saveToBrowserMemory() + // fsState.inMemorySave = true + // fsState.syncFs = false + // fsState.isReadonly = false + // fsState.remoteBackend = false } } From 69cfb89a8dab29018424cdc2fa421a5cea4978e1 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Mon, 26 Aug 2024 03:15:02 +0300 Subject: [PATCH 16/16] display notification on save --- src/react/PauseScreen.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/react/PauseScreen.tsx b/src/react/PauseScreen.tsx index 96b00648..9f25810d 100644 --- a/src/react/PauseScreen.tsx +++ b/src/react/PauseScreen.tsx @@ -22,6 +22,7 @@ import Button from './Button' import Screen from './Screen' import styles from './PauseScreen.module.css' import { DiscordButton } from './DiscordButton' +import { showNotification } from './NotificationProvider' export const saveToBrowserMemory = async () => { setLoadingScreenStatus('Saving world') @@ -33,10 +34,12 @@ export const saveToBrowserMemory = async () => { const allRootPaths = [...usedServerPathsV1] const allFilesToCopy = [] as string[] for (const dirBase of allRootPaths) { + // eslint-disable-next-line no-await-in-loop if (dirBase.includes('.') && await fileExistsAsyncOptimized(join(worldFolder, dirBase))) { allFilesToCopy.push(dirBase) continue } + // eslint-disable-next-line no-await-in-loop let res = await collectFilesToCopy(join(worldFolder, dirBase), true) if (dirBase === 'region') { res = res.filter(x => x.endsWith('.mca')) @@ -133,7 +136,10 @@ export default () => { } const action = await showOptionsModal('World actions...', ['Save to browser memory']) if (action === 'Save to browser memory') { - await saveToBrowserMemory() + const path = await saveToBrowserMemory() + if (!path) return + const saveName = path.split('/').at(-1) + showNotification(`World saved to ${saveName}`, 'Load it to keep your progress!') // fsState.inMemorySave = true // fsState.syncFs = false // fsState.isReadonly = false