From fba6cfb3926105c3cca86180f3521c07df494cf1 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Mon, 28 Aug 2023 13:29:08 +0300 Subject: [PATCH 01/19] test ci: sync publish with ci steps --- .github/workflows/ci.yml | 1 - .github/workflows/publish.yml | 11 +++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae3577a4..a4704ce9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,4 +21,3 @@ jobs: with: name: cypress-images path: cypress/integration/__image_snapshots__/ - if-no-files-found: ignore diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ccfc7311..17e718ca 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,8 +13,15 @@ jobs: run: npm i -g pnpm - run: pnpm install - run: pnpm build - # todo use nohup and official action? - # - run: pnpm prod-start & pnpm test:cypress + - uses: cypress-io/github-action@v5 + with: + install: false + start: pnpm prod-start + - uses: actions/upload-artifact@v3 + if: failure() + with: + name: cypress-images + path: cypress/integration/__image_snapshots__/ - uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} From fbb378865b26f636c872ae933f882dd2927030bd Mon Sep 17 00:00:00 2001 From: Vitaly Date: Mon, 28 Aug 2023 13:30:49 +0300 Subject: [PATCH 02/19] move localServerOptions into options namespace --- src/index.js | 2 +- src/optionsStorage.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index d41ef51f..984df736 100644 --- a/src/index.js +++ b/src/index.js @@ -358,7 +358,7 @@ async function connect (connectOptions) { window.serverDataChannel ??= {} window.worldLoaded = false //@ts-ignore TODO - Object.assign(serverOptions, _.defaultsDeep(JSON.parse(localStorage.localServerOptions || '{}'), connectOptions.serverOverrides, serverOptions)) + Object.assign(serverOptions, _.defaultsDeep(JSON.parse(options.localServerOptions || '{}'), connectOptions.serverOverrides, serverOptions)) singlePlayerServer = window.singlePlayerServer = startLocalServer() // todo need just to call quit if started // loadingScreen.maybeRecoverable = false diff --git a/src/optionsStorage.js b/src/optionsStorage.js index 7adb8182..5bc671d7 100644 --- a/src/optionsStorage.js +++ b/src/optionsStorage.js @@ -14,7 +14,8 @@ const defaultOptions = { maxMultiplayerRenderDistance: 6, excludeCommunicationDebugEvents: [], preventDevReloadWhilePlaying: false, - numWorkers: 4 + numWorkers: 4, + localServerOptions: {} } export const options = proxy( From 9102cc8db8c4b31d31da7abc8234c44ab0d009ad Mon Sep 17 00:00:00 2001 From: Vitaly Date: Mon, 28 Aug 2023 13:52:07 +0300 Subject: [PATCH 03/19] test error fix --- cypress/integration/index.spec.ts | 11 +++++++---- esbuild.mjs | 10 +++++----- minecraft-server.mjs | 1 - src/defaultLocalServerOptions.js | 1 - src/index.js | 3 +-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/cypress/integration/index.spec.ts b/cypress/integration/index.spec.ts index 41f8e96d..53505b3c 100644 --- a/cypress/integration/index.spec.ts +++ b/cypress/integration/index.spec.ts @@ -3,15 +3,18 @@ it('Loads & renders singleplayer', () => { // todo use ` } - onBtnClick () { + onBtnClick (e) { playSound('click_stereo.mp3') - this.dispatchEvent(new window.CustomEvent('pmui-click')) + this.dispatchEvent(new window.CustomEvent('pmui-click', { detail: e, })) } } diff --git a/src/menus/title_screen.js b/src/menus/title_screen.js index 204f0f86..fd7bf963 100644 --- a/src/menus/title_screen.js +++ b/src/menus/title_screen.js @@ -1,9 +1,12 @@ -const { openWorldDirectory } = require('../browserfs') +const { openWorldDirectory, openWorldZip } = require('../browserfs') const { showModal } = require('../globalState') const { fsState } = require('../loadFolder') const { openURL } = require('./components/common') const { LitElement, html, css } = require('lit') +// const SUPPORT_WORLD_LOADING = !!window.showDirectoryPicker +const SUPPORT_WORLD_LOADING = true + class TitleScreen extends LitElement { static get styles () { return css` @@ -148,16 +151,42 @@ class TitleScreen extends LitElement { } From 01c56388a539c198a1f1bbc2678b9f031276f8b3 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Wed, 30 Aug 2023 10:07:03 +0300 Subject: [PATCH 15/19] add a way to reset world when loading errored or disconnected --- src/browserfs.js | 18 ++++++++++++ src/builtinCommands.ts | 42 +++++++++++++++------------- src/index.js | 6 ++-- src/menus/loading_or_error_screen.js | 11 +++++++- 4 files changed, 53 insertions(+), 24 deletions(-) diff --git a/src/browserfs.js b/src/browserfs.js index b2140c0a..03c73b15 100644 --- a/src/browserfs.js +++ b/src/browserfs.js @@ -2,6 +2,7 @@ import { fsState, loadFolder } from './loadFolder' import { oneOf } from '@zardoy/utils' import JSZip from 'jszip' +import { join } from 'path' const { promisify } = require('util') const browserfs = require('browserfs') @@ -77,6 +78,23 @@ fs.promises.open = async (...args) => { } } +// for testing purposes, todo move it to core patch +const removeFileRecursiveSync = (path) => { + fs.readdirSync(path).forEach((file) => { + const curPath = join(path, file) + if (fs.lstatSync(curPath).isDirectory()) { + // recurse + removeFileRecursiveSync(curPath) + fs.rmdirSync(curPath) + } else { + // delete file + fs.unlinkSync(curPath) + } + }) +} + +window.removeFileRecursiveSync = removeFileRecursiveSync + const SUPPORT_WRITE = true export const openWorldDirectory = async (/** @type {FileSystemDirectoryHandle?} */dragndropHandle = undefined) => { diff --git a/src/builtinCommands.ts b/src/builtinCommands.ts index 518e0d14..61de3366 100644 --- a/src/builtinCommands.ts +++ b/src/builtinCommands.ts @@ -28,28 +28,32 @@ async function addFolderToZip(folderPath, zip, relativePath) { // todo include in help +const exportWorld = async () => { + // todo issue into chat warning if fs is writable! + const zip = new JSZip() + let worldFolder: string = singlePlayerServer.options.worldFolder + if (!worldFolder.startsWith('/')) worldFolder = `/${worldFolder}` + await addFolderToZip(worldFolder, zip, '') + + // Generate the ZIP archive content + const zipContent = await zip.generateAsync({ type: "blob" }) + + // Create a download link and trigger the download + const downloadLink = document.createElement("a") + downloadLink.href = URL.createObjectURL(zipContent) + downloadLink.download = "world-exported.zip" + downloadLink.click() + + // Clean up the URL object after download + URL.revokeObjectURL(downloadLink.href) +} + +window.exportWorld = exportWorld + const commands = [ { command: ['/download', '/export'], - invoke: async () => { - // todo issue into chat warning if fs is writable! - const zip = new JSZip() - let worldFolder: string = singlePlayerServer.options.worldFolder - if (!worldFolder.startsWith('/')) worldFolder = `/${worldFolder}` - await addFolderToZip(worldFolder, zip, '') - - // Generate the ZIP archive content - const zipContent = await zip.generateAsync({ type: "blob" }) - - // Create a download link and trigger the download - const downloadLink = document.createElement("a") - downloadLink.href = URL.createObjectURL(zipContent) - downloadLink.download = "world-exported.zip" - downloadLink.click() - - // Clean up the URL object after download - URL.revokeObjectURL(downloadLink.href) - } + invoke: exportWorld }, { command: ['/publish'], diff --git a/src/index.js b/src/index.js index edfa44da..3060c370 100644 --- a/src/index.js +++ b/src/index.js @@ -48,7 +48,7 @@ const nbt = require('prismarine-nbt') const pathfinder = require('mineflayer-pathfinder') const { Vec3 } = require('vec3') -const Cursor = require('./cursor') +const Cursor = require('./cursor').default //@ts-ignore global.THREE = require('three') const { initVR } = require('./vr') @@ -449,9 +449,7 @@ async function connect (connectOptions) { const d = subscribeKey(options, 'renderDistance', () => { singlePlayerServer.options['view-distance'] = options.renderDistance worldView.viewDistance = options.renderDistance - if (miscUiState.singleplayer) { - window.onPlayerChangeRenderDistance?.(options.renderDistance) - } + window.onPlayerChangeRenderDistance?.(options.renderDistance) }) disposables.push(d) } diff --git a/src/menus/loading_or_error_screen.js b/src/menus/loading_or_error_screen.js index dfbf3660..9519179e 100644 --- a/src/menus/loading_or_error_screen.js +++ b/src/menus/loading_or_error_screen.js @@ -2,8 +2,9 @@ const { LitElement, html, css } = require('lit') const { commonCss } = require('./components/common') const { addPanoramaCubeMap } = require('../panorama') -const { hideModal, activeModalStacks, activeModalStack, replaceActiveModalStack } = require('../globalState') +const { hideModal, activeModalStacks, activeModalStack, replaceActiveModalStack, miscUiState } = require('../globalState') const { guessProblem } = require('../guessProblem') +const { fsState } = require('../loadFolder') class LoadingErrorScreen extends LitElement { static get styles () { @@ -84,6 +85,14 @@ class LoadingErrorScreen extends LitElement { } document.getElementById('play-screen').style.display = 'block' addPanoramaCubeMap() + }}> { + if (!confirm('Are you sure you want to delete all local world content?')) return + for (const key of Object.keys(localStorage)) { + if (/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/g.test(key) || key === '/') { + localStorage.removeItem(key) + } + } + window.location.reload() }}> window.location.reload()} pmui-label="Full Reload" pmui-width="200px">` : '' } From 9c403f9a126edcc6f1370d9e14a857d8a153240c Mon Sep 17 00:00:00 2001 From: Vitaly Date: Wed, 30 Aug 2023 10:43:21 +0300 Subject: [PATCH 16/19] use custom mc loader to drop a few mb of bundle & allow to specify allowlist for faster dev builds (will be cached in future) --- .gitignore | 1 + esbuild.mjs | 18 ++++++-- scripts/esbuildPlugins.mjs | 87 ++++++++++++++++++++++++++++++++++---- scripts/patchPackages.js | 27 ------------ 4 files changed, 94 insertions(+), 39 deletions(-) delete mode 100644 scripts/patchPackages.js diff --git a/.gitignore b/.gitignore index 1a3c5c10..23b666f8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ public .env.local Thumbs.db build +localSettings.mjs dist .DS_Store .idea/ diff --git a/esbuild.mjs b/esbuild.mjs index 9487e7c6..c6760785 100644 --- a/esbuild.mjs +++ b/esbuild.mjs @@ -31,11 +31,16 @@ let baseConfig = {} // outdir: undefined, // } +try { + await import('./localSettings.mjs') +} catch { } + fs.copyFileSync('index.html', 'dist/index.html') fs.writeFileSync('dist/index.html', fs.readFileSync('dist/index.html', 'utf8').replace('', ''), 'utf8') -const dev = process.argv.includes('--watch') || process.argv.includes('-w') +const watch = process.argv.includes('--watch') || process.argv.includes('-w') const prod = process.argv.includes('--prod') +const dev = !prod const banner = [ 'window.global = globalThis;', @@ -64,7 +69,7 @@ const ctx = await esbuild.context({ keepNames: true, ...baseConfig, banner: { - js: banner.join('\n') + js: banner.join('\n'), }, alias: { events: 'events', // make explicit @@ -92,10 +97,15 @@ const ctx = await esbuild.context({ 'process.env.GITHUB_URL': JSON.stringify(`https://github.com/${process.env.GITHUB_REPOSITORY || `${process.env.VERCEL_GIT_REPO_OWNER}/${process.env.VERCEL_GIT_REPO_SLUG}`}`) }, - // chunkNames: '[name]', + loader: { + '.png': 'dataurl' + }, + write: false, + // todo would be better to enable? + // preserveSymlinks: true, }) -if (dev) { +if (watch) { await ctx.watch() server.app.get('/esbuild', (req, res, next) => { res.writeHead(200, { diff --git a/scripts/esbuildPlugins.mjs b/scripts/esbuildPlugins.mjs index 7bb3b2b2..c415db3d 100644 --- a/scripts/esbuildPlugins.mjs +++ b/scripts/esbuildPlugins.mjs @@ -5,7 +5,8 @@ import { join, dirname } from 'path' import * as fs from 'fs' import { filesize } from 'filesize' -let clients = [] +const prod = process.argv.includes('--prod') +let connectedClients = [] /** @type {import('esbuild').Plugin[]} */ const plugins = [ @@ -62,11 +63,20 @@ const plugins = [ } }, { - name: 'assets-resolve', + name: 'data-assets', setup (build) { + const customMcDataNs = 'custom-mc-data' + build.onResolve({ filter: /.*/, }, async ({ path, ...rest }) => { + if (join(rest.resolveDir, path).replaceAll('\\', '/').endsWith('minecraft-data/data.js')) { + return { + namespace: customMcDataNs, + path + } + } + if (path.includes('stage')) return if (['.woff', '.woff2', '.ttf', '.png', '.jpg', '.jpeg', '.gif', '.svg'].some(ext => path.endsWith(ext))) { return { path, @@ -75,8 +85,67 @@ const plugins = [ } } }) - build.onEnd(({ metafile, outputFiles }) => { - const prod = process.argv.includes('--prod') + + build.onLoad({ + filter: /.*/, + namespace: customMcDataNs, + }, async ({ path, ...rest }) => { + const resolvedPath = await build.resolve('minecraft-data/minecraft-data/data/dataPaths.json', { kind: 'require-call', resolveDir: process.cwd() }) + const dataPaths = JSON.parse(await fs.promises.readFile(resolvedPath.path, 'utf8')) + + // bedrock unsupported + delete dataPaths.bedrock + + const allowOnlyList = process.env.ONLY_MC_DATA?.split(',') ?? [] + + // skip data for 0.30c, snapshots and pre-releases + const ignoredVersionsRegex = /(^0\.30c$)|w|-pre|-rc/ + + const includedVersions = [] + let contents = 'module.exports =\n{\n' + for (const platform of Object.keys(dataPaths)) { + contents += ` '${platform}': {\n` + for (const version of Object.keys(dataPaths[platform])) { + if (allowOnlyList.length && !allowOnlyList.includes(version)) continue + if (ignoredVersionsRegex.test(version)) continue + + includedVersions.push(version) + contents += ` '${version}': {\n` + for (const dataType of Object.keys(dataPaths[platform][version])) { + const loc = `minecraft-data/data/${dataPaths[platform][version][dataType]}/` + contents += ` get ${dataType} () { return require("./${loc}${dataType}.json") },\n` + } + contents += ' },\n' + } + contents += ' },\n' + } + contents += '}\n' + + if (prod) { + console.log('Included mc-data versions:', includedVersions) + } + return { + contents, + loader: 'js', + resolveDir: join(dirname(resolvedPath.path), '../..'), + } + }) + + build.onEnd(async ({ metafile, outputFiles }) => { + // write outputFiles + for (const file of outputFiles) { + // if (file.path.endsWith('index.js.map')) { + // const map = JSON.parse(file.text) + // map.sourcesContent = map.sourcesContent.map((c, index) => { + // if (map.sources[index].endsWith('.json')) return '' + // return c + // }) + // // data.sources = data.sources.filter(source => !source.endsWith('.json')) + // await fs.promises.writeFile(file.path, JSON.stringify(map), 'utf8') + // } else { + await fs.promises.writeFile(file.path, file.contents) + // } + } if (!prod) return // const deps = Object.entries(metafile.inputs).sort(([, a], [, b]) => b.bytes - a.bytes).map(([x, { bytes }]) => [x, filesize(bytes)]).slice(0, 5) //@ts-ignore @@ -116,22 +185,24 @@ const plugins = [ }) build.onEnd(({ errors, outputFiles, metafile, warnings }) => { const elapsed = Date.now() - time + // write metafile to disk if needed + // fs.writeFileSync('dist/meta.json', JSON.stringify(metafile, null, 2)) console.log(`Done in ${elapsed}ms`) if (count++ === 0) { return } if (errors.length) { - clients.forEach((res) => { + connectedClients.forEach((res) => { res.write(`data: ${JSON.stringify({ errors: errors.map(error => error.text) })}\n\n`) res.flush() }) return } - clients.forEach((res) => { + connectedClients.forEach((res) => { res.write(`data: ${JSON.stringify({ update: { time: elapsed } })}\n\n`) res.flush() }) - clients.length = 0 + connectedClients.length = 0 }) } }, @@ -236,4 +307,4 @@ const plugins = [ }) ] -export { plugins, clients } +export { plugins, connectedClients as clients } diff --git a/scripts/patchPackages.js b/scripts/patchPackages.js deleted file mode 100644 index 9cdd9f09..00000000 --- a/scripts/patchPackages.js +++ /dev/null @@ -1,27 +0,0 @@ -//@ts-check -const path = require('path') -const dataPath = path.join(require.resolve('minecraft-data'), '../data.js') - -const fs = require('fs') - -const lines = fs.readFileSync(dataPath, 'utf8').split('\n') -if (lines[0] === '//patched') { - console.log('Already patched') - process.exit(0) -} - -function removeLinesBetween (start, end) { - let startIndex = lines.findIndex(line => line === start) - if (startIndex === -1) return - let endIndex = startIndex + lines.slice(startIndex).findIndex(line => line === end) - // insert block comments - lines.splice(startIndex, 0, `/*`) - lines.splice(endIndex + 2, 0, `*/`) -} - -// todo removing bedrock support for now, will optiimze in future instead -removeLinesBetween(" 'bedrock': {", ' }') - -lines.unshift('//patched') -// fs.writeFileSync(path.join(dataPath, '../dataGlobal.js'), newContents, 'utf8') -fs.writeFileSync(dataPath, lines.join('\n'), 'utf8') From ad0b545301db0ff50cf7e2d5aa34c884e64f2c56 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Wed, 30 Aug 2023 10:53:58 +0300 Subject: [PATCH 17/19] copy only entity textures, inline others! it reduces copy time and deploy size --- esbuild.mjs | 1 + package.json | 4 ++-- scripts/build.js | 18 +++++++-------- scripts/esbuildPlugins.mjs | 3 +-- src/cursor.js | 35 ++++++++++++++++++++++++++---- src/globals.d.ts | 9 ++++---- src/menus/components/breath_bar.js | 7 +++--- src/menus/components/button.js | 9 +++++--- src/menus/components/food_bar.js | 5 +++-- src/menus/components/health_bar.js | 5 +++-- src/menus/components/hotbar.js | 8 ++++--- src/menus/components/slider.js | 8 ++++--- src/menus/hud.js | 11 ++++++---- src/menus/title_screen.js | 8 ++++--- src/styles.css | 2 +- 15 files changed, 88 insertions(+), 45 deletions(-) diff --git a/esbuild.mjs b/esbuild.mjs index c6760785..43ee2b1d 100644 --- a/esbuild.mjs +++ b/esbuild.mjs @@ -98,6 +98,7 @@ const ctx = await esbuild.context({ JSON.stringify(`https://github.com/${process.env.GITHUB_REPOSITORY || `${process.env.VERCEL_GIT_REPO_OWNER}/${process.env.VERCEL_GIT_REPO_SLUG}`}`) }, loader: { + // todo use external or resolve issues with duplicating '.png': 'dataurl' }, write: false, diff --git a/package.json b/package.json index ba239575..d09a2db9 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,6 @@ "start-watch-script": "nodemon -w esbuild.mjs esbuild.mjs", "build": "node scripts/build.js copyFiles && node esbuild.mjs --minify --prod", "watch": "node scripts/build.js copyFilesDev && webpack serve --config webpack.dev.js --progress", - "postinstall": "node scripts/patchPackages.js", "test:cypress": "cypress run", "test:e2e": "start-test http-get://localhost:8080 test:cypress", "prod-start": "node server.js", @@ -74,7 +73,7 @@ "https-browserify": "^1.0.0", "lodash-webpack-plugin": "^0.11.6", "memfs": "^3.5.3", - "mineflayer": "^4.11.0", + "mineflayer": "github:zardoy/mineflayer#custom", "mineflayer-pathfinder": "^2.4.4", "npm-run-all": "^4.1.5", "os-browserify": "^0.3.0", @@ -98,6 +97,7 @@ "pnpm": { "overrides": { "minecraft-data": "latest", + "prismarine-provider-anvil": "github:zardoy/prismarine-provider-anvil#chunk-impl-require-fix", "minecraft-protocol": "github:zardoy/minecraft-protocol#custom-client-extra" } } diff --git a/scripts/build.js b/scripts/build.js index ffd1c8dc..9f30fd36 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -12,7 +12,7 @@ const filesAlwaysToCopy = [ // these files could be copied at build time eg with copy plugin, but copy plugin slows down the config (2x in my testing, sometimes with too many open files error) is slow so we also copy them there const webpackFilesToCopy = [ { from: './node_modules/prismarine-viewer2/public/blocksStates/', to: 'dist/blocksStates/' }, - { from: './node_modules/prismarine-viewer2/public/textures/', to: 'dist/textures/' }, + // { from: './node_modules/prismarine-viewer2/public/textures/', to: 'dist/textures/' }, { from: './node_modules/prismarine-viewer2/public/worker.js', to: 'dist/worker.js' }, { from: './node_modules/prismarine-viewer2/public/supportedVersions.json', to: 'dist/supportedVersions.json' }, { from: './assets/', to: './dist/' }, @@ -24,6 +24,13 @@ exports.copyFiles = (isDev = false) => { [...filesAlwaysToCopy, ...webpackFilesToCopy].forEach(file => { fsExtra.copySync(file.from, file.to) }) + const cwd = './node_modules/prismarine-viewer2/public/textures/' + const files = glob.sync('{*/entity/**,*.png}', { cwd: cwd, nodir: true, }) + for (const file of files) { + const copyDest = path.join('dist/textures/', file) + fs.mkdirSync(path.dirname(copyDest), { recursive: true, }) + fs.copyFileSync(path.join(cwd, file), copyDest) + } console.timeEnd('copy files') } @@ -48,15 +55,8 @@ exports.getSwAdditionalEntries = () => { '*.png', '*.woff', 'worker.js', - // todo add gui textures (1.17.1) - // todo if we uncomment it it will spam the server with requests for textures on initial page load - // we need to put all textures into on file instead! - // `textures/${singlePlayerVersion}/**`, - `textures/${singlePlayerVersion}/blocks/destroy_stage_0.png.png`, - `textures/${singlePlayerVersion}/blocks/destroy_stage_1.png.png`, + // todo-low preload entity atlas? `textures/${singlePlayerVersion}.png`, - `textures/1.16.4/gui/widgets.png`, - `textures/1.16.4/gui/icons.png`, `textures/1.16.4/entity/squid.png`, ] const filesNeedsCacheKey = [ diff --git a/scripts/esbuildPlugins.mjs b/scripts/esbuildPlugins.mjs index c415db3d..42ca70b9 100644 --- a/scripts/esbuildPlugins.mjs +++ b/scripts/esbuildPlugins.mjs @@ -76,8 +76,7 @@ const plugins = [ path } } - if (path.includes('stage')) return - if (['.woff', '.woff2', '.ttf', '.png', '.jpg', '.jpeg', '.gif', '.svg'].some(ext => path.endsWith(ext))) { + if (['.woff', '.woff2', '.ttf'].some(ext => path.endsWith(ext))) { return { path, namespace: 'assets', diff --git a/src/cursor.js b/src/cursor.js index 4c47de7a..bb3c1bbe 100644 --- a/src/cursor.js +++ b/src/cursor.js @@ -1,7 +1,20 @@ +//@ts-check /* global THREE performance */ -const { Vec3 } = require('vec3') -const { isGameActive } = require('./globalState') +// wouldn't better to create atlas instead? +import destroyStage0 from 'minecraft-assets/minecraft-assets/data/1.10/blocks/destroy_stage_0.png' +import destroyStage1 from 'minecraft-assets/minecraft-assets/data/1.10/blocks/destroy_stage_1.png' +import destroyStage2 from 'minecraft-assets/minecraft-assets/data/1.10/blocks/destroy_stage_2.png' +import destroyStage3 from 'minecraft-assets/minecraft-assets/data/1.10/blocks/destroy_stage_3.png' +import destroyStage4 from 'minecraft-assets/minecraft-assets/data/1.10/blocks/destroy_stage_4.png' +import destroyStage5 from 'minecraft-assets/minecraft-assets/data/1.10/blocks/destroy_stage_5.png' +import destroyStage6 from 'minecraft-assets/minecraft-assets/data/1.10/blocks/destroy_stage_6.png' +import destroyStage7 from 'minecraft-assets/minecraft-assets/data/1.10/blocks/destroy_stage_7.png' +import destroyStage8 from 'minecraft-assets/minecraft-assets/data/1.10/blocks/destroy_stage_8.png' +import destroyStage9 from 'minecraft-assets/minecraft-assets/data/1.10/blocks/destroy_stage_9.png' + +import { Vec3 } from 'vec3' +import { isGameActive } from './globalState' function getViewDirection (pitch, yaw) { const csPitch = Math.cos(pitch) @@ -27,8 +40,20 @@ class Cursor { const loader = new THREE.TextureLoader() this.breakTextures = [] + const destroyStagesImages = [ + destroyStage0, + destroyStage1, + destroyStage2, + destroyStage3, + destroyStage4, + destroyStage5, + destroyStage6, + destroyStage7, + destroyStage8, + destroyStage9 + ] for (let i = 0; i < 10; i++) { - const texture = loader.load('textures/' + viewer.version + '/blocks/destroy_stage_' + i + '.png') + const texture = loader.load(destroyStagesImages[i]) texture.magFilter = THREE.NearestFilter texture.minFilter = THREE.NearestFilter this.breakTextures.push(texture) @@ -96,8 +121,10 @@ class Cursor { // Place if (cursorBlock && this.buttons[2] && (!this.lastButtons[2] || cursorChanged) && this.lastBlockPlaced >= 4) { const vecArray = [new Vec3(0, -1, 0), new Vec3(0, 1, 0), new Vec3(0, 0, -1), new Vec3(0, 0, 1), new Vec3(-1, 0, 0), new Vec3(1, 0, 0)] + //@ts-ignore const delta = cursorBlock.intersect.minus(cursorBlock.position) // check instead? + //@ts-ignore bot._placeBlockWithOptions(cursorBlock, vecArray[cursorBlock.face], { delta, forceLook: 'ignore' }).catch(console.warn) // this.lastBlockPlaced = 0 } @@ -148,4 +175,4 @@ class Cursor { } } -module.exports = Cursor +export default Cursor diff --git a/src/globals.d.ts b/src/globals.d.ts index db486c6a..cc13b180 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -1,6 +1,6 @@ /// -declare const THREE: typeof import('three'); +declare const THREE: typeof import('three') // todo declare const bot: import('mineflayer').Bot declare const singlePlayerServer: any @@ -30,9 +30,10 @@ interface ObjectConstructor { } declare module '*.css' { - /** - * @deprecated Use `import style from './style.css?inline'` instead. - */ const css: string export default css } +declare module '*.png' { + const png: string + export default png +} diff --git a/src/menus/components/breath_bar.js b/src/menus/components/breath_bar.js index d0ff1394..3c03602c 100644 --- a/src/menus/components/breath_bar.js +++ b/src/menus/components/breath_bar.js @@ -1,4 +1,5 @@ -const { LitElement, html, css } = require('lit') +const { LitElement, html, css, unsafeCSS } = require('lit') +const { guiIcons1_17_1 } = require('../hud') class BreathBar extends LitElement { static get styles () { @@ -22,13 +23,13 @@ class BreathBar extends LitElement { } .breath.full { - background-image: url('textures/1.17.1/gui/icons.png'); + background-image: url('${unsafeCSS(guiIcons1_17_1)}'); background-size: 256px; background-position: var(--offset) var(--bg-y); } .breath.half { - background-image: url('textures/1.17.1/gui/icons.png'); + background-image: url('${unsafeCSS(guiIcons1_17_1)}'); background-size: 256px; background-position: calc(var(--offset) - 9) var(--bg-y); } diff --git a/src/menus/components/button.js b/src/menus/components/button.js index dbbb693a..0ab40280 100644 --- a/src/menus/components/button.js +++ b/src/menus/components/button.js @@ -1,4 +1,6 @@ -const { LitElement, html, css } = require('lit') +//@ts-check +const widgetsGui = require('minecraft-assets/minecraft-assets/data/1.17.1/gui/widgets.png') +const { LitElement, html, css, unsafeCSS } = require('lit') const audioContext = new window.AudioContext() const sounds = {} @@ -74,7 +76,7 @@ class Button extends LitElement { left: 0; width: calc(50% + 1px); height: 20px; - background: url('textures/1.17.1/gui/widgets.png'); + background: url('${unsafeCSS(widgetsGui)}'); background-size: 256px; background-position-y: calc(var(--txrV) * -1); z-index: -1; @@ -88,7 +90,7 @@ class Button extends LitElement { left: 50%; width: 50%; height: 20px; - background: url('textures/1.17.1/gui/widgets.png'); + background: url('${unsafeCSS(widgetsGui)}'); background-size: 256px; background-position-x: calc(-200px + 100%); background-position-y: calc(var(--txrV) * -1); @@ -131,6 +133,7 @@ class Button extends LitElement { constructor () { super() this.label = '' + this.icon = undefined this.disabled = false this.width = '200px' this.onPress = () => { } diff --git a/src/menus/components/food_bar.js b/src/menus/components/food_bar.js index 17bdcccb..7f71ad0f 100644 --- a/src/menus/components/food_bar.js +++ b/src/menus/components/food_bar.js @@ -1,4 +1,5 @@ -const { LitElement, html, css } = require('lit') +const { LitElement, html, css, unsafeCSS } = require('lit') +const { guiIcons1_17_1 } = require('../hud') class FoodBar extends LitElement { static get styles () { @@ -19,7 +20,7 @@ class FoodBar extends LitElement { .food { width: 9px; height: 9px; - background-image: url('textures/1.17.1/gui/icons.png'), url('textures/1.17.1/gui/icons.png'); + background-image: url('${unsafeCSS(guiIcons1_17_1)}'), url('${unsafeCSS(guiIcons1_17_1)}'); background-size: 256px, 256px; background-position: var(--bg-x) var(--bg-y), var(--bg-x) var(--bg-y); margin-left: -1px; diff --git a/src/menus/components/health_bar.js b/src/menus/components/health_bar.js index fdbc3b8c..a6dacef8 100644 --- a/src/menus/components/health_bar.js +++ b/src/menus/components/health_bar.js @@ -1,4 +1,5 @@ -const { LitElement, html, css } = require('lit') +const { LitElement, html, css, unsafeCSS } = require('lit') +const { guiIcons1_17_1 } = require('../hud') function getEffectClass (effect) { switch (effect.id) { @@ -49,7 +50,7 @@ class HealthBar extends LitElement { .heart { width: 9px; height: 9px; - background-image: url('textures/1.17.1/gui/icons.png'), url('textures/1.17.1/gui/icons.png'); + background-image: url('${unsafeCSS(guiIcons1_17_1)}'), url('${unsafeCSS(guiIcons1_17_1)}'); background-size: 256px, 256px; background-position: var(--bg-x) var(--bg-y), var(--bg-x) var(--bg-y); margin-left: -1px; diff --git a/src/menus/components/hotbar.js b/src/menus/components/hotbar.js index d347b4f9..32b78606 100644 --- a/src/menus/components/hotbar.js +++ b/src/menus/components/hotbar.js @@ -1,7 +1,9 @@ -const { LitElement, html, css } = require('lit') +const { LitElement, html, css, unsafeCSS } = require('lit') const invsprite = require('../../invsprite.json') const { isGameActive } = require('../../globalState') +const widgetsTexture = require('minecraft-assets/minecraft-assets/data/1.16.4/gui/widgets.png') + class Hotbar extends LitElement { static get styles () { return css` @@ -12,7 +14,7 @@ class Hotbar extends LitElement { transform: translate(-50%); width: 182px; height: 22px; - background: url("textures/1.16.4/gui/widgets.png"); + background: url("${unsafeCSS(widgetsTexture)}"); background-size: 256px; } @@ -22,7 +24,7 @@ class Hotbar extends LitElement { top: -1px; width: 24px; height: 24px; - background: url("textures/1.16.4/gui/widgets.png"); + background: url("${unsafeCSS(widgetsTexture)}"); background-size: 256px; background-position-y: -22px; } diff --git a/src/menus/components/slider.js b/src/menus/components/slider.js index 25ced293..bb72f9f2 100644 --- a/src/menus/components/slider.js +++ b/src/menus/components/slider.js @@ -1,4 +1,6 @@ -const { LitElement, html, css } = require('lit') +const { LitElement, html, css, unsafeCSS } = require('lit') + +const widgetsGui = require('minecraft-assets/minecraft-assets/data/1.17.1/gui/widgets.png') class Slider extends LitElement { static get styles () { @@ -39,7 +41,7 @@ class Slider extends LitElement { left: 0; width: 50%; height: 20px; - background: url('textures/1.17.1/gui/widgets.png'); + background: url('${unsafeCSS(widgetsGui)}'); background-size: 256px; background-position-y: var(--txrV); z-index: -1; @@ -54,7 +56,7 @@ class Slider extends LitElement { left: 50%; width: 50%; height: 20px; - background: url('textures/1.17.1/gui/widgets.png'); + background: url('${unsafeCSS(widgetsGui)}'); background-size: 256px; background-position-x: calc(-200px + 100%); background-position-y: var(--txrV); diff --git a/src/menus/hud.js b/src/menus/hud.js index f465b50e..fa82f86f 100644 --- a/src/menus/hud.js +++ b/src/menus/hud.js @@ -1,10 +1,13 @@ //@ts-check -const { LitElement, html, css } = require('lit') +const { LitElement, html, css, unsafeCSS } = require('lit') const { isMobile } = require('./components/common') const { showModal, miscUiState } = require('../globalState') const { options, watchValue } = require('../optionsStorage') const { getGamemodeNumber } = require('../utils') +export const guiIcons1_17_1 = require('minecraft-assets/minecraft-assets/data/1.17.1/gui/icons.png') +export const guiIcons1_16_4 = require('minecraft-assets/minecraft-assets/data/1.16.4/gui/icons.png') + class Hud extends LitElement { static get styles () { return css` @@ -21,7 +24,7 @@ class Hud extends LitElement { .crosshair { width: 16px; height: 16px; - background: url('textures/1.17.1/gui/icons.png'); + background: url('${unsafeCSS(guiIcons1_17_1)}'); background-size: 256px; position: absolute; top: 50%; @@ -49,7 +52,7 @@ class Hud extends LitElement { transform: translate(-50%); width: 182px; height: 5px; - background-image: url('textures/1.16.4/gui/icons.png'); + background-image: url('${unsafeCSS(guiIcons1_16_4)}'); background-size: 256px; background-position-y: -64px; } @@ -57,7 +60,7 @@ class Hud extends LitElement { .xp-bar { width: 182px; height: 5px; - background-image: url('textures/1.17.1/gui/icons.png'); + background-image: url('${unsafeCSS(guiIcons1_17_1)}'); background-size: 256px; background-position-y: -69px; } diff --git a/src/menus/title_screen.js b/src/menus/title_screen.js index f1d8a52b..93c6e1ff 100644 --- a/src/menus/title_screen.js +++ b/src/menus/title_screen.js @@ -2,7 +2,9 @@ const { openWorldDirectory, openWorldZip } = require('../browserfs') const { showModal } = require('../globalState') const { fsState } = require('../loadFolder') const { openURL } = require('./components/common') -const { LitElement, html, css } = require('lit') +const { LitElement, html, css, unsafeCSS } = require('lit') + +const mcImage = require('minecraft-assets/minecraft-assets/data/1.17.1/gui/title/minecraft.png') // const SUPPORT_WORLD_LOADING = !!window.showDirectoryPicker const SUPPORT_WORLD_LOADING = true @@ -21,7 +23,7 @@ class TitleScreen extends LitElement { position: absolute; top: 0; left: 0; - background-image: url('textures/1.17.1/gui/title/minecraft.png'); + background-image: url('${unsafeCSS(mcImage)}'); background-size: 256px; width: 155px; height: 44px; @@ -32,7 +34,7 @@ class TitleScreen extends LitElement { position: absolute; top: 0; left: 155px; - background-image: url('textures/1.17.1/gui/title/minecraft.png'); + background-image: url('${unsafeCSS(mcImage)}'); background-size: 256px; width: 155px; height: 44px; diff --git a/src/styles.css b/src/styles.css index 7f2b0a59..726d4aef 100644 --- a/src/styles.css +++ b/src/styles.css @@ -26,7 +26,7 @@ html { position: absolute; top: 0; left: 0; - background: url('textures/1.17.1/gui/options_background.png'), rgba(0, 0, 0, 0.7); + background: url('minecraft-assets/minecraft-assets/data/1.17.1/gui/options_background.png'), rgba(0, 0, 0, 0.7); background-size: 16px; background-repeat: repeat; width: 100%; From bb4ce869649261237c30fcf59efffab0ffb30fae Mon Sep 17 00:00:00 2001 From: Vitaly Date: Wed, 30 Aug 2023 12:15:04 +0300 Subject: [PATCH 18/19] almost fix annoying key controls issue --- src/botControls.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/botControls.js b/src/botControls.js index ec7e0ffa..d901b2f6 100644 --- a/src/botControls.js +++ b/src/botControls.js @@ -128,7 +128,7 @@ document.addEventListener('keydown', (e) => { }) document.addEventListener('keyup', (e) => { - if (!isGameActive(true)) return + // if (!isGameActive(true)) return keyBindScrn.keymaps.forEach(km => { if (e.code === km.key) { From a7ab42e6d2e244be1ebe2583dc25b6c8d2297301 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Wed, 30 Aug 2023 12:20:29 +0300 Subject: [PATCH 19/19] singleplayer saves: fix loading player data from level.dat (todo: not saving for now) respect superflat world generation 1.8 superflat saves tested: seems stable! --- src/browserfs.js | 16 ++++++++++++++-- src/globalState.js | 2 +- src/loadFolder.ts | 33 ++++++++++++++++++++++++++++++--- src/utils.js | 16 ++++++++++++++++ 4 files changed, 61 insertions(+), 6 deletions(-) diff --git a/src/browserfs.js b/src/browserfs.js index 03c73b15..05792684 100644 --- a/src/browserfs.js +++ b/src/browserfs.js @@ -20,6 +20,8 @@ browserfs.configure({ if (e) throw e }) +export const forceCachedDataPaths = {} + //@ts-ignore fs.promises = new Proxy(Object.fromEntries(['readFile', 'writeFile', 'stat', 'mkdir', 'rename', /* 'copyFile', */'readdir'].map(key => [key, promisify(fs[key])])), { get (target, p, receiver) { @@ -30,7 +32,17 @@ fs.promises = new Proxy(Object.fromEntries(['readFile', 'writeFile', 'stat', 'mk if (typeof args[0] === 'string' && !args[0].startsWith('/')) args[0] = '/' + args[0] // Write methods // todo issue one-time warning (in chat I guess) - if (oneOf(p, 'writeFile', 'mkdir', 'rename') && fsState.isReadonly) return + if (fsState.isReadonly) { + if (oneOf(p, 'readFile', 'writeFile') && forceCachedDataPaths[args[0]]) { + if (p === 'readFile') { + return Promise.resolve(forceCachedDataPaths[args[0]]) + } else if (p === 'writeFile') { + forceCachedDataPaths[args[0]] = args[1] + return Promise.resolve() + } + } + if (oneOf(p, 'writeFile', 'mkdir', 'rename')) return + } if (p === 'open' && fsState.isReadonly) { args[1] = 'r' // read-only, zipfs throw otherwise } @@ -183,7 +195,7 @@ export const openWorldZip = async (/** @type {File} */file) => { loadFolder(`/world/${availableWorlds[0]}`) } - alert(`${availableWorlds.length} worlds found in the zip, please select one!`) + alert(`Many (${availableWorlds.length}) worlds found in the zip!`) // todo prompt picker // const selectWorld } diff --git a/src/globalState.js b/src/globalState.js index b28083da..5d27332e 100644 --- a/src/globalState.js +++ b/src/globalState.js @@ -132,7 +132,7 @@ const savePlayers = () => { setInterval(() => { savePlayers() // todo investigate unload failures instead -}, 1000) +}, 2000) window.addEventListener('unload', (e) => { savePlayers() diff --git a/src/loadFolder.ts b/src/loadFolder.ts index 491822f7..c48bbac9 100644 --- a/src/loadFolder.ts +++ b/src/loadFolder.ts @@ -4,6 +4,9 @@ import * as nbt from 'prismarine-nbt' import { promisify } from 'util' import { options } from './optionsStorage' import { proxy } from 'valtio' +import { nameToMcOfflineUUID } from './utils' +import { forceCachedDataPaths } from './browserfs' +import { gzip } from 'node-gzip' const parseNbt = promisify(nbt.parse) @@ -16,10 +19,15 @@ export const fsState = proxy({ const PROPOSE_BACKUP = true export const loadFolder = async (root = '/world') => { - // todo-low cache reading + // todo do it in singleplayer as well + for (const key in forceCachedDataPaths) { + delete forceCachedDataPaths[key] + } + const warnings: string[] = [] let levelDatContent try { + // todo-low cache reading levelDatContent = await fs.promises.readFile(`${root}/level.dat`) } catch (err) { if (err.code === 'ENOENT') { @@ -33,11 +41,12 @@ export const loadFolder = async (root = '/world') => { } } - let version: string | undefined + let isFlat = false if (levelDatContent) { const parsedRaw = await parseNbt(Buffer.from(levelDatContent)) const levelDat: import('./mcTypes').LevelDat = nbt.simplify(parsedRaw).Data + version = levelDat.Version?.Name if (!version) { const newVersion = prompt(`In 1.8 and before world save doesn\'t contain version info, please enter version you want to use to load the world.\nSupported versions ${supportedVersions.join(', ')}`, '1.8.8') @@ -48,7 +57,6 @@ export const loadFolder = async (root = '/world') => { warnings.push(`Version ${version} is not supported, supported versions ${supportedVersions.join(', ')}, 1.16.1 will be used`) version = '1.16.1' } - let isFlat = false if (levelDat.WorldGenSettings) { for (const [key, value] of Object.entries(levelDat.WorldGenSettings.dimensions)) { if (key.slice(10) === 'overworld') { @@ -64,6 +72,20 @@ export const loadFolder = async (root = '/world') => { if (!isFlat) { warnings.push(`Generator ${levelDat.generatorName} is not supported yet`) } + + const playerUuid = nameToMcOfflineUUID(options.localUsername) + const playerDatPath = `${root}/playerdata/${playerUuid}.dat` + try { + await fs.promises.stat(playerDatPath) + } catch (err) { + const playerDat = await gzip(nbt.writeUncompressed({ name: '', ...(parsedRaw.value.Data.value as Record).Player })) + if (fsState.isReadonly) { + forceCachedDataPaths[playerDatPath] = playerDat + } else { + await fs.promises.writeFile(playerDatPath, playerDat) + } + } + } if (warnings.length) { @@ -98,6 +120,11 @@ export const loadFolder = async (root = '/world') => { // todo check gamemode level.dat data etc detail: { version, + ...isFlat ? { + generation: { + name: 'superflat' + } + } : {}, ...root !== '/world' ? { 'worldFolder': root } : {} diff --git a/src/utils.js b/src/utils.js index cf129d24..30c0d41c 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,8 @@ //@ts-check import { activeModalStack, miscUiState } from './globalState' import { notification } from './menus/notification' +import * as crypto from 'crypto' +import UUID from 'uuid-1345' export const goFullscreen = async (doToggle = false) => { if (!document.fullscreenElement) { @@ -116,3 +118,17 @@ export const getGamemodeNumber = (bot) => { export const isCypress = () => { return localStorage.cypress === 'true' } + +// https://github.com/PrismarineJS/node-minecraft-protocol/blob/cf1f67117d586b5e6e21f0d9602da12e9fcf46b6/src/server/login.js#L170 +function javaUUID (s) { + const hash = crypto.createHash('md5') + hash.update(s, 'utf8') + const buffer = hash.digest() + buffer[6] = (buffer[6] & 0x0f) | 0x30 + buffer[8] = (buffer[8] & 0x3f) | 0x80 + return buffer +} + +export function nameToMcOfflineUUID (name) { + return (new UUID(javaUUID('OfflinePlayer:' + name))).toString() +}