diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 000000000..b98d140b1 --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,41 @@ +name: Vercel Deploy Preview +env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} +on: + issue_comment: + types: [created] + push: + branches: + - perf-test +jobs: + deploy: + runs-on: ubuntu-latest + # todo skip already created deploys on that commit + if: >- + github.event.issue.pull_request != '' && + ( + contains(github.event.comment.body, '/benchmark') + ) + permissions: + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + ref: refs/pull/${{ github.event.issue.number }}/head + - run: npm i -g pnpm@9.0.4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + cache: "pnpm" + - run: pnpm install + - run: pnpm build + - run: pnpm test:benchmark + # read benchmark results from stdout + - run: echo "BENCHMARK_RESULT=$(cat benchmark.txt)" >> $GITHUB_ENV + - uses: mshick/add-pr-comment@v2 + with: + allow-repeats: true + message: | + Benchmark result: ${{ env.BENCHMARK_RESULT }} diff --git a/cypress.config.ts b/cypress.config.ts index f9bd94783..7c4098a9f 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -1,5 +1,7 @@ import { defineConfig } from 'cypress' +const isPerformanceTest = process.env.PERFORMANCE_TEST === 'true' + export default defineConfig({ video: false, chromeWebSecurity: false, @@ -31,7 +33,7 @@ export default defineConfig({ return require('./cypress/plugins/index.js')(on, config) }, baseUrl: 'http://localhost:8080', - specPattern: 'cypress/e2e/**/*.spec.ts', + specPattern: !isPerformanceTest ? 'cypress/e2e/**/*.spec.ts' : 'cypress/e2e/rendering_performance.spec.ts', excludeSpecPattern: ['**/__snapshots__/*', '**/__image_snapshots__/*'], }, }) diff --git a/cypress/e2e/rendering_performance.spec.ts b/cypress/e2e/rendering_performance.spec.ts new file mode 100644 index 000000000..4a7390ab5 --- /dev/null +++ b/cypress/e2e/rendering_performance.spec.ts @@ -0,0 +1,37 @@ +/// +import { BenchmarkAdapter } from '../../src/benchmarkAdapter' +import { setOptions, cleanVisit, visit } from './shared' + +it('Benchmark rendering performance', () => { + cleanVisit('/?openBenchmark=true&renderDistance=5') + // wait for render end event + return cy.document().then({ timeout: 120_000 }, doc => { + return new Cypress.Promise(resolve => { + cy.log('Waiting for world to load') + doc.addEventListener('cypress-world-ready', resolve) + }).then(() => { + cy.log('World loaded') + }) + }).then(() => { + cy.window().then(win => { + const adapter = win.benchmarkAdapter as BenchmarkAdapter + const renderTimeWorst = adapter.worstRenderTime + const renderTimeAvg = adapter.averageRenderTime + const fpsWorst = 1000 / renderTimeWorst + const fpsAvg = 1000 / renderTimeAvg + const totalTime = adapter.worldLoadTime + + const messages = [ + `Worst FPS: ${fpsWorst.toFixed(2)}`, + `Average FPS: ${fpsAvg.toFixed(2)}`, + `Total time: ${totalTime.toFixed(2)}s`, + `Memory usage average: ${adapter.memoryUsageAverage.toFixed(2)}MB`, + `Memory usage worst: ${adapter.memoryUsageWorst.toFixed(2)}MB`, + ] + for (const message of messages) { + cy.log(message) + } + cy.writeFile('benchmark.txt', messages.join('\n')) + }) + }) +}) diff --git a/package.json b/package.json index c03422604..f3b68bf49 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "build": "node scripts/build.js copyFiles && node scripts/prepareData.mjs -f && node esbuild.mjs --minify --prod", "check-build": "tsc && pnpm build", "test:cypress": "cypress run", + "test:benchmark": "PERFORMANCE_TEST=true cypress run", "test-unit": "vitest", "test:e2e": "start-test http-get://localhost:8080 test:cypress", "prod-start": "node server.js", @@ -63,7 +64,7 @@ "esbuild-plugin-polyfill-node": "^0.3.0", "express": "^4.18.2", "filesize": "^10.0.12", - "flying-squid": "npm:@zardoy/flying-squid@^0.0.29", + "flying-squid": "npm:@zardoy/flying-squid@^0.0.32", "fs-extra": "^11.1.1", "google-drive-browserfs": "github:zardoy/browserfs#google-drive", "iconify-icon": "^1.0.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1f2b48099..a22220966 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -107,8 +107,8 @@ importers: specifier: ^10.0.12 version: 10.0.12 flying-squid: - specifier: npm:@zardoy/flying-squid@^0.0.29 - version: '@zardoy/flying-squid@0.0.29(encoding@0.1.13)' + specifier: npm:@zardoy/flying-squid@^0.0.32 + version: '@zardoy/flying-squid@0.0.32(encoding@0.1.13)' fs-extra: specifier: ^11.1.1 version: 11.1.1 @@ -3078,8 +3078,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.29': - resolution: {integrity: sha512-E5Nk1gMeH+fAHM5aJY8kIxjBS/zuPtPD6QPeZg+laPV5H58Jx3Et17clF1zC9MT2wyFQ5wi5uTnfdGBTpSEqHw==} + '@zardoy/flying-squid@0.0.32': + resolution: {integrity: sha512-Ifj8XrnsE3j3+lCeyUQ426LzsOzU/Z+qKG+aZNf90VstBhCvjmVAOmG7J5N74ivvujx+x6eXCgjjw6gcd/XKNQ==} engines: {node: '>=8'} hasBin: true @@ -6750,6 +6750,11 @@ packages: version: 1.35.0 engines: {node: '>=14'} + prismarine-chunk@https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/cea0b6c792d7dcbb69dfd20fa48be5fd60ce83ef: + resolution: {tarball: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/cea0b6c792d7dcbb69dfd20fa48be5fd60ce83ef} + version: 1.35.0 + engines: {node: '>=14'} + prismarine-entity@2.3.1: resolution: {integrity: sha512-HOv8l7IetHNf4hwZ7V/W4vM3GNl+e6VCtKDkH9h02TRq7jWngsggKtJV+VanCce/sNwtJUhJDjORGs728ep4MA==} @@ -11974,7 +11979,7 @@ snapshots: '@types/emscripten': 1.39.8 tslib: 1.14.1 - '@zardoy/flying-squid@0.0.29(encoding@0.1.13)': + '@zardoy/flying-squid@0.0.32(encoding@0.1.13)': dependencies: '@tootallnate/once': 2.0.0 change-case: 4.1.2 @@ -11989,7 +11994,7 @@ snapshots: mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 - prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/9662306deea57d8d0ba0a2a3f3f7adb95f0131e3(minecraft-data@3.65.0) + prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/cea0b6c792d7dcbb69dfd20fa48be5fd60ce83ef(minecraft-data@3.65.0) prismarine-entity: 2.3.1 prismarine-item: 1.14.0 prismarine-nbt: 2.5.0 @@ -12000,6 +12005,7 @@ snapshots: random-seed: 0.3.0 range: 0.0.3 readline: 1.3.0 + sanitize-filename: 1.6.3 typed-emitter: 1.4.0 uuid-1345: 1.0.2 vec3: 0.1.8 @@ -13263,7 +13269,7 @@ snapshots: diamond-square@https://codeload.github.com/zardoy/diamond-square/tar.gz/4bbe28dcad35403abaa925055e91f601a61b9015: dependencies: minecraft-data: 3.65.0 - prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/9662306deea57d8d0ba0a2a3f3f7adb95f0131e3(minecraft-data@3.65.0) + prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/cea0b6c792d7dcbb69dfd20fa48be5fd60ce83ef(minecraft-data@3.65.0) prismarine-registry: 1.7.0 random-seed: 0.3.0 vec3: 0.1.8 @@ -16592,6 +16598,19 @@ snapshots: transitivePeerDependencies: - minecraft-data + prismarine-chunk@https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/cea0b6c792d7dcbb69dfd20fa48be5fd60ce83ef(minecraft-data@3.65.0): + dependencies: + prismarine-biome: 1.3.0(minecraft-data@3.65.0)(prismarine-registry@1.7.0) + prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/dd4954fff3b334f8ce063d18e39b2e9414ece5b8 + prismarine-nbt: 2.5.0 + prismarine-registry: 1.7.0 + smart-buffer: 4.2.0 + uint4: 0.1.2 + vec3: 0.1.8 + xxhash-wasm: 0.4.2 + transitivePeerDependencies: + - minecraft-data + prismarine-entity@2.3.1: dependencies: prismarine-chat: 1.10.1 diff --git a/prismarine-viewer/viewer/lib/viewerWrapper.ts b/prismarine-viewer/viewer/lib/viewerWrapper.ts index 57317f422..3b042f4a6 100644 --- a/prismarine-viewer/viewer/lib/viewerWrapper.ts +++ b/prismarine-viewer/viewer/lib/viewerWrapper.ts @@ -52,9 +52,11 @@ export class ViewerWrapper { windowFocused = true trackWindowFocus () { window.addEventListener('focus', () => { + console.log('window focused') this.windowFocused = true }) window.addEventListener('blur', () => { + console.log('window blurred') this.windowFocused = false }) } diff --git a/prismarine-viewer/viewer/lib/worldrendererThree.ts b/prismarine-viewer/viewer/lib/worldrendererThree.ts index cc89a8238..9f5b94b53 100644 --- a/prismarine-viewer/viewer/lib/worldrendererThree.ts +++ b/prismarine-viewer/viewer/lib/worldrendererThree.ts @@ -17,6 +17,8 @@ export class WorldRendererThree extends WorldRendererCommon { signsCache = new Map() starField: StarField cameraSectionPos: Vec3 = new Vec3(0, 0, 0) + worstRenderTime = 0 + avgRenderTime = 0 get tilesRendered () { return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).tilesCount, 0) @@ -72,20 +74,6 @@ 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 (!this.initialChunksLoad && this.enableChunksLoadDelay) { - // const newPromise = new Promise(resolve => { - // if (this.droppedFpsPercentage > 0.5) { - // setTimeout(resolve, 1000 / 50 * this.droppedFpsPercentage) - // } else { - // setTimeout(resolve) - // } - // }) - // this.promisesQueue.push(newPromise) - // for (const promise of this.promisesQueue) { - // await promise - // } - // } - const geometry = new THREE.BufferGeometry() geometry.setAttribute('position', new THREE.BufferAttribute(data.geometry.positions, 3)) geometry.setAttribute('normal', new THREE.BufferAttribute(data.geometry.normals, 3)) @@ -163,7 +151,11 @@ export class WorldRendererThree extends WorldRendererCommon { render () { tweenJs.update() const cam = this.camera instanceof THREE.Group ? this.camera.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera + const start = performance.now() this.renderer.render(this.scene, cam) + const totalTime = performance.now() - start + this.avgRenderTime = this.avgRenderTime * 0.9 + totalTime * 0.1 // exponential moving average + this.worstRenderTime = Math.max(this.worstRenderTime, totalTime) } renderSign (position: Vec3, rotation: number, isWall: boolean, isHanging: boolean, blockEntity) { diff --git a/src/benchmark.ts b/src/benchmark.ts new file mode 100644 index 000000000..a9a342d61 --- /dev/null +++ b/src/benchmark.ts @@ -0,0 +1,73 @@ +import { Vec3 } from 'vec3' +import { downloadAndOpenFileFromUrl } from './downloadAndOpenFile' +import { activeModalStack, miscUiState } from './globalState' +import { options } from './optionsStorage' +import { BenchmarkAdapter } from './benchmarkAdapter' + +const testWorldFixtureUrl = 'https://bucket.mcraft.fun/Future CITY 4.4-slim.zip' +const testWorldFixtureSpawn = [-133, 87, 309] as const + +export const openBenchmark = async (renderDistance = 8) => { + let memoryUsageAverage = 0 + let memoryUsageSamples = 0 + let memoryUsageWorst = 0 + setInterval(() => { + const memoryUsage = (window.performance as any)?.memory?.usedJSHeapSize + if (memoryUsage) { + memoryUsageAverage = (memoryUsageAverage * memoryUsageSamples + memoryUsage) / (memoryUsageSamples + 1) + memoryUsageSamples++ + if (memoryUsage > memoryUsageWorst) { + memoryUsageWorst = memoryUsage + } + } + }, 200) + + const benchmarkAdapter: BenchmarkAdapter = { + get worldLoadTime () { + return window.worldLoadTime + }, + get averageRenderTime () { + return window.viewer.world.avgRenderTime + }, + get worstRenderTime () { + return window.viewer.world.worstRenderTime + }, + get memoryUsageAverage () { + return memoryUsageAverage + }, + get memoryUsageWorst () { + return memoryUsageWorst + } + } + window.benchmarkAdapter = benchmarkAdapter + + options.renderDistance = renderDistance + void downloadAndOpenFileFromUrl(testWorldFixtureUrl, undefined, { + connectEvents: { + serverCreated () { + if (testWorldFixtureSpawn) { + localServer!.spawnPoint = new Vec3(...testWorldFixtureSpawn) + localServer!.on('newPlayer', (player) => { + player.on('dataLoaded', () => { + player.position = new Vec3(...testWorldFixtureSpawn) + }) + }) + } + }, + } + }) +} + +export const registerOpenBenchmarkListener = () => { + const params = new URLSearchParams(window.location.search) + if (params.get('openBenchmark')) { + void openBenchmark(params.has('renderDistance') ? +params.get('renderDistance')! : undefined) + } + + window.addEventListener('keydown', (e) => { + if (e.code === 'KeyB' && e.shiftKey && !miscUiState.gameLoaded && activeModalStack.length === 0) { + e.preventDefault() + void openBenchmark() + } + }) +} diff --git a/src/benchmarkAdapter.ts b/src/benchmarkAdapter.ts new file mode 100644 index 000000000..9eef1121c --- /dev/null +++ b/src/benchmarkAdapter.ts @@ -0,0 +1,7 @@ +export interface BenchmarkAdapter { + worldLoadTime: number + averageRenderTime: number + worstRenderTime: number + memoryUsageAverage: number + memoryUsageWorst: number +} diff --git a/src/browserfs.ts b/src/browserfs.ts index ebe8acfdd..aa2c0e9e9 100644 --- a/src/browserfs.ts +++ b/src/browserfs.ts @@ -10,6 +10,7 @@ import { fsState, loadSave } from './loadSave' import { installTexturePack, installTexturePackFromHandle, updateTexturePackInstalledState } from './texturePack' import { miscUiState } from './globalState' import { setLoadingScreenStatus } from './utils' +import { ConnectOptions } from './connect' const { GoogleDriveFileSystem } = require('google-drive-browserfs/src/backends/GoogleDrive') // disable type checking browserfs.install(window) @@ -434,7 +435,7 @@ export const copyFilesAsync = async (pathSrc: string, pathDest: string, fileCopi } // todo rename method -const openWorldZipInner = async (file: File | ArrayBuffer, name = file['name']) => { +const openWorldZipInner = async (file: File | ArrayBuffer, name = file['name'], connectOptions?: Partial) => { await new Promise(async resolve => { browserfs.configure({ // todo @@ -478,7 +479,7 @@ const openWorldZipInner = async (file: File | ArrayBuffer, name = file['name']) } if (availableWorlds.length === 1) { - await loadSave(`/world/${availableWorlds[0]}`) + await loadSave(`/world/${availableWorlds[0]}`, connectOptions) return } diff --git a/src/connect.ts b/src/connect.ts index 5e4df8599..fc716c843 100644 --- a/src/connect.ts +++ b/src/connect.ts @@ -1,15 +1,24 @@ export type ConnectOptions = { - server?: string; - singleplayer?: any; - username: string; - password?: any; - proxy?: any; - botVersion?: any; - serverOverrides?; - serverOverridesFlat?; - peerId?: string; - ignoreQs?: boolean; + server?: string + singleplayer?: any + username: string + password?: any + proxy?: any + botVersion?: any + serverOverrides? + serverOverridesFlat? + peerId?: string + ignoreQs?: boolean onSuccessfulPlay?: () => void autoLoginPassword?: string serverIndex?: string + + connectEvents?: { + serverCreated?: () => void + // connect: () => void; + // disconnect: () => void; + // error: (err: any) => void; + // ready: () => void; + // end: () => void; + } } diff --git a/src/downloadAndOpenFile.ts b/src/downloadAndOpenFile.ts index 7ac154fcb..ad0f831c4 100644 --- a/src/downloadAndOpenFile.ts +++ b/src/downloadAndOpenFile.ts @@ -2,27 +2,25 @@ import prettyBytes from 'pretty-bytes' import { openWorldZip } from './browserfs' import { getResourcePackName, installTexturePack, resourcePackState, updateTexturePackInstalledState } from './texturePack' import { setLoadingScreenStatus } from './utils' +import { ConnectOptions } from './connect' export const getFixedFilesize = (bytes: number) => { return prettyBytes(bytes, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) } -const inner = async () => { - const qs = new URLSearchParams(window.location.search) - let mapUrl = qs.get('map') - const texturepack = qs.get('texturepack') +export const downloadAndOpenFileFromUrl = async (mapUrl: string | undefined, texturepackUrl: string | undefined, connectOptions?: Partial) => { // fixme - if (texturepack) mapUrl = texturepack + if (texturepackUrl) mapUrl = texturepackUrl if (!mapUrl) return false - if (texturepack) { + if (texturepackUrl) { await updateTexturePackInstalledState() if (resourcePackState.resourcePackInstalled) { if (!confirm(`You are going to install a new resource pack, which will REPLACE the current one: ${await getResourcePackName()} Continue?`)) return } } const name = mapUrl.slice(mapUrl.lastIndexOf('/') + 1).slice(-25) - const downloadThing = texturepack ? 'texturepack' : 'world' + const downloadThing = texturepackUrl ? 'texturepack' : 'world' setLoadingScreenStatus(`Downloading ${downloadThing} ${name}...`) const response = await fetch(mapUrl) @@ -63,17 +61,20 @@ const inner = async () => { }, }) ).arrayBuffer() - if (texturepack) { + if (texturepackUrl) { const name = mapUrl.slice(mapUrl.lastIndexOf('/') + 1).slice(-30) await installTexturePack(buffer, name) } else { - await openWorldZip(buffer) + await openWorldZip(buffer, undefined, connectOptions) } } export default async () => { try { - return await inner() + const qs = new URLSearchParams(window.location.search) + const mapUrl = qs.get('map') + const texturepack = qs.get('texturepack') + return await downloadAndOpenFileFromUrl(mapUrl ?? undefined, texturepack ?? undefined) } catch (err) { setLoadingScreenStatus(`Failed to download. Either refresh page or remove map param from URL. Reason: ${err.message}`) return true diff --git a/src/index.ts b/src/index.ts index 72f270d42..4c667c6fe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -42,6 +42,7 @@ import debug from 'debug' import { defaultsDeep } from 'lodash-es' import { initVR } from './vr' +import { registerOpenBenchmarkListener } from './benchmark' import { AppConfig, activeModalStack, @@ -218,8 +219,14 @@ function hideCurrentScreens () { insertActiveModalStack('', []) } -const loadSingleplayer = (serverOverrides = {}, flattenedServerOverrides = {}) => { - void connect({ singleplayer: true, username: options.localUsername, password: '', serverOverrides, serverOverridesFlat: flattenedServerOverrides }) +const loadSingleplayer = (serverOverrides = {}, flattenedServerOverrides = {}, otherOptions: Partial = {}) => { + void connect({ + singleplayer: true, + username: options.localUsername, + serverOverrides, + serverOverridesFlat: flattenedServerOverrides, + ...otherOptions + }) } function listenGlobalEvents () { window.addEventListener('connect', e => { @@ -227,7 +234,9 @@ function listenGlobalEvents () { void connect(options) }) window.addEventListener('singleplayer', (e) => { - loadSingleplayer((e as CustomEvent).detail) + const { detail } = (e as CustomEvent) + const { connectOptions, ...rest } = detail + loadSingleplayer(rest, {}, connectOptions) }) } @@ -406,6 +415,7 @@ async function connect (connectOptions: ConnectOptions) { setLoadingScreenStatus('Starting local server') localServer = window.localServer = window.server = startLocalServer(serverOptions) + connectOptions?.connectEvents?.serverCreated?.() // todo need just to call quit if started // loadingScreen.maybeRecoverable = false // init world, todo: do it for any async plugins @@ -578,6 +588,7 @@ async function connect (connectOptions: ConnectOptions) { const spawnEarlier = !singleplayer && !p2pMultiplayer // don't use spawn event, player can be dead bot.once(spawnEarlier ? 'forcedMove' : 'health', () => { + window.worldStartLoad = Date.now() errorAbortController.abort() const mcData = MinecraftData(bot.version) window.PrismarineBlock = PrismarineBlock(mcData.version.minecraftVersion!) @@ -805,7 +816,9 @@ async function connect (connectOptions: ConnectOptions) { // todo might not emit as servers simply don't send chunk if it's empty if (!viewer.world.allChunksFinished || done) return done = true - console.log('All done and ready! In', (Date.now() - start) / 1000, 's') + const worldLoadTime = (Date.now() - start) / 1000 + window.worldLoadTime = worldLoadTime + console.log('All done and ready! In', worldLoadTime, 's') viewer.render() // ensure the last state is rendered document.dispatchEvent(new Event('cypress-world-ready')) }) @@ -944,7 +957,7 @@ downloadAndOpenFile().then((downloadAction) => { }) }, (err) => { console.error(err) - alert(`Failed to download file: ${err}`) + alert(`Somethin went wrong: ${err}`) }) // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion @@ -956,3 +969,4 @@ if (initialLoader) { window.pageLoaded = true void possiblyHandleStateVariable() +registerOpenBenchmarkListener() diff --git a/src/loadSave.ts b/src/loadSave.ts index 7ca454ff6..5f3241df1 100644 --- a/src/loadSave.ts +++ b/src/loadSave.ts @@ -11,6 +11,7 @@ import { isMajorVersionGreater } from './utils' import { activeModalStacks, insertActiveModalStack, miscUiState } from './globalState' import supportedVersions from './supportedVersions.mjs' +import { ConnectOptions } from './connect' // todo include name of opened handle (zip)! // additional fs metadata @@ -46,7 +47,7 @@ export const readLevelDat = async (path) => { return { levelDat, dataRaw: parsed.value.Data!.value as Record } } -export const loadSave = async (root = '/world') => { +export const loadSave = async (root = '/world', connectOptions?: Partial) => { // todo test if (miscUiState.gameLoaded) { await disconnect() @@ -189,7 +190,8 @@ export const loadSave = async (root = '/world') => { } : {}, ...root === '/world' ? {} : { 'worldFolder': root - } + }, + connectOptions }, })) }