diff --git a/README.MD b/README.MD index 94c4987ce..946ce5f60 100644 --- a/README.MD +++ b/README.MD @@ -53,11 +53,14 @@ There is world renderer playground ([link](https://mcon.vercel.app/playground.ht However, there are many things that can be done in online version. You can access some global variables in the console and useful examples: -- `localStorage.debug = '*'` - Enables all debug messages! +- `localStorage.debug = '*'` - Enables all debug messages! Warning: this will start all packets spam. +Instead I recommend setting `options.debugLogNotFrequentPackets`. Also you can use `debugTopPackets` (with JSON.stringify) to see what packets were received/sent by name - `bot` - Mineflayer bot instance. See Mineflayer documentation for more. - `viewer` - Three.js viewer instance, basically does all the rendering. - `viewer.world.sectionObjects` - Object with all active chunk sections (geometries) in the world. Each chunk section is a Three.js mesh or group. +- `debugSceneChunks` - The same as above, but relative to current bot position (e.g. 0,0 is the current chunk). +- `debugChangedOptions` - See what options are changed. Don't change options here. - `localServer` - Only for singleplayer mode/host. Flying Squid server instance, see it's documentation for more. - `localServer.overworld.storageProvider.regions` - See ALL LOADED region files with all raw data. @@ -70,7 +73,7 @@ You can also drag and drop any .dat file into the browser window to see it's con - `F3` - Toggle debug overlay - `F3 + A` - Reload all chunks (these that are loaded from the server) -- `F3 + G` - Toggle chunk sections (geometries) border visibility (aka Three.js geometry helpers) - most probably need to reload chunks after toggling +- `F3 + G` - Toggle chunk sections (geometries) border visibility (aka Three.js geometry helpers) ### Notable Things that Power this Project diff --git a/package.json b/package.json index 869e8059b..579876af5 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,9 @@ "start": "node scripts/build.js copyFilesDev && node scripts/prepareData.mjs && node esbuild.mjs --watch", "start-watch-script": "nodemon -w esbuild.mjs esbuild.mjs", "build": "node scripts/build.js copyFiles && node scripts/prepareData.mjs -f && node esbuild.mjs --minify --prod", - "check-build": "tsc && pnpm build", + "check-build": "tsc && pnpm test-unit && pnpm build", "test:cypress": "cypress run", + "test-unit": "vitest", "test:e2e": "start-test http-get://localhost:8080 test:cypress", "prod-start": "node server.js", "postinstall": "node scripts/gen-texturepack-files.mjs && tsx scripts/optimizeBlockCollisions.ts", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3fdc74ce2..33337d5b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -73,7 +73,7 @@ importers: version: 4.18.2 flying-squid: specifier: github:zardoy/space-squid#everything - version: github.com/zardoy/space-squid/a639714c26e2252b34be833f64f23d9f45f136de + version: github.com/zardoy/space-squid/9d72f865da99bcc55db2c5071754f61a5d935c73 fs-extra: specifier: ^11.1.1 version: 11.1.1 @@ -4604,7 +4604,7 @@ packages: /@types/connect@3.4.36: resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==} dependencies: - '@types/node': 20.8.0 + '@types/node': 20.8.10 dev: true /@types/cookie@0.4.1: @@ -4620,7 +4620,7 @@ packages: /@types/cross-spawn@6.0.3: resolution: {integrity: sha512-BDAkU7WHHRHnvBf5z89lcvACsvkz/n7Tv+HyD/uW76O29HoH1Tk/W6iQrepaZVbisvlEek4ygwT8IW7ow9XLAA==} dependencies: - '@types/node': 20.8.0 + '@types/node': 20.8.10 dev: true /@types/detect-port@1.3.3: @@ -4691,7 +4691,7 @@ packages: /@types/graceful-fs@4.1.7: resolution: {integrity: sha512-MhzcwU8aUygZroVwL2jeYk6JisJrPl/oov/gsgGCue9mkgl9wjGbzReYQClxiUgFDnib9FuHqTndccKeZKxTRw==} dependencies: - '@types/node': 20.8.0 + '@types/node': 20.8.10 dev: true /@types/http-cache-semantics@4.0.2: @@ -4844,7 +4844,7 @@ packages: /@types/resolve@1.17.1: resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} dependencies: - '@types/node': 20.8.0 + '@types/node': 20.8.10 dev: false /@types/sat@0.0.31: @@ -4862,7 +4862,7 @@ packages: resolution: {integrity: sha512-aAG6yRf6r0wQ29bkS+x97BIs64ZLxeE/ARwyS6wrldMm3C1MdKwCcnnEwMC1slI8wuxJOpiUH9MioC0A0i+GJw==} dependencies: '@types/mime': 1.3.3 - '@types/node': 20.8.0 + '@types/node': 20.8.10 dev: true /@types/serve-static@1.15.3: @@ -9606,7 +9606,7 @@ packages: resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 20.8.0 + '@types/node': 20.8.10 merge-stream: 2.0.0 supports-color: 7.2.0 dev: false @@ -9615,7 +9615,7 @@ packages: resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@types/node': 20.8.0 + '@types/node': 20.8.10 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -14750,13 +14750,12 @@ packages: - utf-8-validate dev: false - github.com/zardoy/space-squid/a639714c26e2252b34be833f64f23d9f45f136de: - resolution: {tarball: https://codeload.github.com/zardoy/space-squid/tar.gz/a639714c26e2252b34be833f64f23d9f45f136de} + github.com/zardoy/space-squid/9d72f865da99bcc55db2c5071754f61a5d935c73: + resolution: {tarball: https://codeload.github.com/zardoy/space-squid/tar.gz/9d72f865da99bcc55db2c5071754f61a5d935c73} name: flying-squid version: 0.0.0-dev engines: {node: '>=8'} hasBin: true - prepare: true requiresBuild: true dependencies: change-case: 4.1.2 diff --git a/prismarine-viewer/examples/playground.ts b/prismarine-viewer/examples/playground.ts index db2d1c7bd..27726625a 100644 --- a/prismarine-viewer/examples/playground.ts +++ b/prismarine-viewer/examples/playground.ts @@ -90,19 +90,18 @@ async function main () { // const schem = await Schematic.read(Buffer.from(data), version) const viewDistance = 0 - const center = new Vec3(0, 90, 0) + const targetPos = new Vec3(2, 90, 2) const World = WorldLoader(version) // const diamondSquare = require('diamond-square')({ version, seed: Math.floor(Math.random() * Math.pow(2, 31)) }) - const targetPos = center //@ts-ignore const chunk1 = new Chunk() //@ts-ignore const chunk2 = new Chunk() - chunk1.setBlockStateId(center, 34) - chunk2.setBlockStateId(center.offset(1, 0, 0), 34) + chunk1.setBlockStateId(targetPos, 34) + chunk2.setBlockStateId(targetPos.offset(1, 0, 0), 34) const world = new World((chunkX, chunkZ) => { // if (chunkX === 0 && chunkZ === 0) return chunk1 // if (chunkX === 1 && chunkZ === 0) return chunk2 @@ -113,7 +112,7 @@ async function main () { // await schem.paste(world, new Vec3(0, 60, 0)) - const worldView = new WorldDataEmitter(world, viewDistance, center) + const worldView = new WorldDataEmitter(world, viewDistance, targetPos) // Create three.js context, add to page const renderer = new THREE.WebGLRenderer() @@ -127,20 +126,20 @@ async function main () { viewer.listen(worldView) // Load chunks - await worldView.init(center) + await worldView.init(targetPos) window['worldView'] = worldView window['viewer'] = viewer //@ts-ignore const controls = new globalThis.THREE.OrbitControls(viewer.camera, renderer.domElement) - controls.target.set(center.x + 0.5, center.y + 0.5, center.z + 0.5) + controls.target.set(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5) - const cameraPos = center.offset(2, 2, 2) + const cameraPos = targetPos.offset(2, 2, 2) const pitch = THREE.MathUtils.degToRad(-45) const yaw = THREE.MathUtils.degToRad(45) viewer.camera.rotation.set(pitch, yaw, 0, 'ZYX') - viewer.camera.lookAt(center.x + 0.5, center.y + 0.5, center.z + 0.5) + viewer.camera.lookAt(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5) viewer.camera.position.set(cameraPos.x + 0.5, cameraPos.y + 0.5, cameraPos.z + 0.5) controls.update() @@ -158,30 +157,30 @@ async function main () { const { states } = mcData.blocksByStateId[getBlock()?.minStateId] ?? {} folder = gui.addFolder('metadata') if (states) { - for (const state of states) { - let defaultValue - switch (state.type) { - case 'enum': - defaultValue = state.values[0] - break - case 'bool': - defaultValue = false - break - case 'int': - defaultValue = 0 - break - case 'direction': - defaultValue = 'north' - break + for (const state of states) { + let defaultValue + switch (state.type) { + case 'enum': + defaultValue = state.values[0] + break + case 'bool': + defaultValue = false + break + case 'int': + defaultValue = 0 + break + case 'direction': + defaultValue = 'north' + break - default: - continue - } - blockProps[state.name] = defaultValue - if (state.type === 'enum') { - folder.add(blockProps, state.name, state.values) - } else { - folder.add(blockProps, state.name) + default: + continue + } + blockProps[state.name] = defaultValue + if (state.type === 'enum') { + folder.add(blockProps, state.name, state.values) + } else { + folder.add(blockProps, state.name) } } } else { @@ -214,8 +213,8 @@ async function main () { } } else { try { - //@ts-ignore - block = Block.fromProperties(blockId ?? -1, blockProps, 0) + //@ts-ignore + block = Block.fromProperties(blockId ?? -1, blockProps, 0) } catch (err) { console.error(err) block = Block.fromStateId(0, 0) diff --git a/prismarine-viewer/viewer/lib/entities.js b/prismarine-viewer/viewer/lib/entities.js index ed9459d90..e46ce437a 100644 --- a/prismarine-viewer/viewer/lib/entities.js +++ b/prismarine-viewer/viewer/lib/entities.js @@ -38,6 +38,7 @@ function getEntityMesh (entity, scene, options) { tex.needsUpdate = true const spriteMat = new THREE.SpriteMaterial({ map: tex }) const sprite = new THREE.Sprite(spriteMat) + sprite.renderOrder = 1000 sprite.scale.set(canvas.width * 0.005, canvas.height * 0.005, 1) sprite.position.y += entity.height + 0.6 diff --git a/prismarine-viewer/viewer/lib/viewer.ts b/prismarine-viewer/viewer/lib/viewer.ts index 3ff8e6caa..92860fbf4 100644 --- a/prismarine-viewer/viewer/lib/viewer.ts +++ b/prismarine-viewer/viewer/lib/viewer.ts @@ -20,6 +20,7 @@ export class Viewer { playerHeight: number isSneaking: boolean version: string + cameraObjectOverride?: THREE.Object3D // for xr constructor (public renderer: THREE.WebGLRenderer, numWorkers?: number) { this.scene = new THREE.Scene() @@ -81,12 +82,13 @@ export class Viewer { } setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number, roll = 0) { + const cam = this.cameraObjectOverride || this.camera if (pos) { let y = pos.y + this.playerHeight if (this.isSneaking) y -= 0.3 - new tweenJs.Tween(this.camera.position).to({ x: pos.x, y, z: pos.z }, 50).start() + new tweenJs.Tween(cam.position).to({ x: pos.x, y, z: pos.z }, 50).start() } - this.camera.rotation.set(pitch, yaw, roll, 'ZYX') + cam.rotation.set(pitch, yaw, roll, 'ZYX') } // todo type @@ -116,6 +118,10 @@ export class Viewer { this.setBlockStateId(new Vec3(pos.x, pos.y, pos.z), stateId) }) + emitter.on('chunkPosUpdate', ({ pos }) => { + this.world.updateViewerPosition(pos) + }) + emitter.emit('listening') this.domElement.addEventListener('pointerdown', (evt) => { diff --git a/prismarine-viewer/viewer/lib/worldDataEmitter.ts b/prismarine-viewer/viewer/lib/worldDataEmitter.ts index 3911696de..9ecb1f436 100644 --- a/prismarine-viewer/viewer/lib/worldDataEmitter.ts +++ b/prismarine-viewer/viewer/lib/worldDataEmitter.ts @@ -18,7 +18,7 @@ export class WorldDataEmitter extends EventEmitter { private eventListeners: Record = {}; private emitter: WorldDataEmitter - constructor (public world: import('prismarine-world').world.World | typeof __type_bot['world'], public viewDistance: number, position: Vec3 = new Vec3(0, 0, 0)) { + constructor(public world: import('prismarine-world').world.World | typeof __type_bot['world'], public viewDistance: number, position: Vec3 = new Vec3(0, 0, 0)) { super() this.loadedChunks = {} this.lastPos = new Vec3(0, 0, 0).update(position) @@ -91,6 +91,7 @@ export class WorldDataEmitter extends EventEmitter { } async init (pos: Vec3) { + this.emitter.emit('chunkPosUpdate', { pos }) const [botX, botZ] = chunkPos(pos) const positions = generateSpiralMatrix(this.viewDistance).map(([x, z]) => new Vec3((botX + x) * 16, 0, (botZ + z) * 16)) @@ -138,6 +139,7 @@ export class WorldDataEmitter extends EventEmitter { const [lastX, lastZ] = chunkPos(this.lastPos) 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 chunksToUnload: Vec3[] = [] for (const coords of Object.keys(this.loadedChunks)) { diff --git a/prismarine-viewer/viewer/lib/worldrenderer.ts b/prismarine-viewer/viewer/lib/worldrenderer.ts index 79c7265da..3f022a732 100644 --- a/prismarine-viewer/viewer/lib/worldrenderer.ts +++ b/prismarine-viewer/viewer/lib/worldrenderer.ts @@ -9,6 +9,7 @@ import { dispose3 } from './dispose' import { toMajor } from './version.js' import PrismarineChatLoader from 'prismarine-chat' import { renderSign } from '../sign-renderer/' +import { chunkPos } from './simpleUtils' function mod (x, n) { return ((x % n) + n) % n @@ -31,8 +32,10 @@ export class WorldRenderer { downloadedBlockStatesData = undefined as any downloadedTextureImage = undefined as any workers: any[] = [] + viewerPosition?: Vec3 texturesVersion?: string + constructor (public scene: THREE.Scene, numWorkers = 4) { // init workers for (let i = 0; i < numWorkers; i++) { @@ -67,13 +70,16 @@ export class WorldRenderer { const mesh = new THREE.Mesh(geometry, this.material) mesh.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz) + mesh.name = 'mesh' object = new THREE.Group() object.add(mesh) - if (this.showChunkBorders) { - const boxHelper = new THREE.BoxHelper(mesh, 0xffff00) - object.add(boxHelper) + const boxHelper = new THREE.BoxHelper(mesh, 0xffff00) + boxHelper.name = 'helper' + object.add(boxHelper) + if (!this.showChunkBorders) { + boxHelper.visible = false } - // should not it compute once + // should not compute it once if (Object.keys(data.geometry.signs).length) { for (const [posKey, { isWall, rotation }] of Object.entries(data.geometry.signs)) { const [x, y, z] = posKey.split(',') @@ -83,6 +89,7 @@ export class WorldRenderer { } } this.sectionObjects[data.key] = object + this.updatePosDataChunk(data.key) this.scene.add(object) } else if (data.type === 'sectionFinished') { this.sectionsOutstanding.delete(data.key) @@ -94,6 +101,26 @@ export class WorldRenderer { } } + /** + * Optionally update data that are depedendent on the viewer position + */ + updatePosDataChunk (key: string) { + if (!this.viewerPosition) return + const [x, y, z] = key.split(',').map(x => Math.floor(+x / 16)) + const [xPlayer, yPlayer, zPlayer] = this.viewerPosition.toArray().map(x => Math.floor(x / 16)) + // sum of distances: x + y + z + const chunkDistance = Math.abs(x - xPlayer) + Math.abs(y - yPlayer) + Math.abs(z - zPlayer) + const section = this.sectionObjects[key].children.find(child => child.name === 'mesh')! + section.renderOrder = 500 - chunkDistance + } + + updateViewerPosition (pos: Vec3) { + this.viewerPosition = pos + for (const key of Object.keys(this.sectionObjects)) { + this.updatePosDataChunk(key) + } + } + renderSign (position: Vec3, rotation: number, isWall: boolean, blockEntity) { const PrismarineChat = PrismarineChatLoader(this.version!) const canvas = renderSign(blockEntity, PrismarineChat) @@ -124,6 +151,17 @@ export class WorldRenderer { return group } + updateShowChunksBorder (value: boolean) { + this.showChunkBorders = value + for (const object of Object.values(this.sectionObjects)) { + for (const child of object.children) { + if (child.name === 'helper') { + child.visible = value; + } + } + } + } + resetWorld () { this.active = false for (const mesh of Object.values(this.sectionObjects)) { @@ -182,6 +220,15 @@ export class WorldRenderer { }) } + getLoadedChunksRelative (pos: Vec3) { + const [currentX, currentZ] = chunkPos(pos) + return Object.fromEntries(Object.entries(this.sectionObjects).map(([key, o]) => { + const [xRaw, yRaw, zRaw] = key.split(',').map(Number) + const [x, z] = chunkPos({x: xRaw, z: zRaw}) + return [`${x - currentX},${z - currentZ}`, o] + })) + } + addColumn (x, z, chunk) { this.loadedChunks[`${x},${z}`] = true for (const worker of this.workers) { diff --git a/prismarine-viewer/viewer/sign-renderer/index.ts b/prismarine-viewer/viewer/sign-renderer/index.ts index 5471ccf43..b4200c5de 100644 --- a/prismarine-viewer/viewer/sign-renderer/index.ts +++ b/prismarine-viewer/viewer/sign-renderer/index.ts @@ -57,7 +57,8 @@ export const renderSign = (blockEntity: SignBlockEntity, PrismarineChat: typeof const defaultColor = ('front_text' in blockEntity ? blockEntity.front_text.color : blockEntity.Color) || 'black' for (let [lineNum, text] of texts.slice(0, 4).entries()) { // todo: in pre flatenning it seems the format was not json - const parsed = text?.startsWith('{') ? parseSafe(text ?? '""', 'sign text') : text + if (text === 'null') continue + const parsed = text?.startsWith('{') || text?.startsWith('"') ? parseSafe(text ?? '""', 'sign text') : text if (!parsed || (typeof parsed !== 'object' && typeof parsed !== 'string')) continue // todo fix type const message = typeof parsed === 'string' ? fromFormattedString(parsed) : new PrismarineChat(parsed) as never diff --git a/prismarine-viewer/viewer/sign-renderer/tests.test.ts b/prismarine-viewer/viewer/sign-renderer/tests.test.ts new file mode 100644 index 000000000..03eb43943 --- /dev/null +++ b/prismarine-viewer/viewer/sign-renderer/tests.test.ts @@ -0,0 +1,85 @@ +import { test, expect } from 'vitest' +import { renderSign } from '.' +import PrismarineChatLoader from 'prismarine-chat' + +const PrismarineChat = PrismarineChatLoader({ language: {} } as any) +let ctxTexts = [] as any[] + +global.document = { + createElement () { + return { + getContext () { + return { + fillText (text, x, y) { + ctxTexts.push({ text, x, y }) + }, + measureText () { return 0 } + } + } + } + } +} as any + +const render = (entity) => { + ctxTexts = [] + renderSign(entity, PrismarineChat) + return ctxTexts.map(({ text, y }) => [y / 80, text]) +} + +test('sign renderer', () => { + let blockEntity = { + "GlowingText": 0, + "Color": "black", + "Text4": "{\"text\":\"\"}", + "Text3": "{\"text\":\"\"}", + "Text2": "{\"text\":\"\"}", + "Text1": "{\"extra\":[{\"color\":\"dark_green\",\"text\":\"Minecraft \"},{\"text\":\"Tools\"}],\"text\":\"\"}" + } as any + expect(render(blockEntity)).toMatchInlineSnapshot(` + [ + [ + 1, + "", + ], + [ + 1, + "Minecraft ", + ], + [ + 1, + "Tools", + ], + [ + 2, + "", + ], + [ + 3, + "", + ], + [ + 4, + "", + ], + ] + `) + + blockEntity = { // pre flatenning + "Text1": "Welcome to", + "Text2": "", + "Text3": "null", + "Text4": "\"Version 2.1\"", + } as const + expect(render(blockEntity)).toMatchInlineSnapshot(` + [ + [ + 1, + "Welcome to", + ], + [ + 4, + "Version 2.1", + ], + ] + `) +}) diff --git a/src/basicSounds.ts b/src/basicSounds.ts index b5a7f535d..2a084692c 100644 --- a/src/basicSounds.ts +++ b/src/basicSounds.ts @@ -17,6 +17,10 @@ export async function loadSound (path: string) { } export async function playSound (path) { + const volume = options.volume / 100 + + if (!volume) return + audioContext ??= new window.AudioContext() for (const [soundName, sound] of Object.entries(sounds)) { @@ -25,8 +29,6 @@ export async function playSound (path) { convertedSounds.push(soundName) } - const volume = options.volume / 100 - const soundBuffer = sounds[path] if (!soundBuffer) { console.warn(`Sound ${path} not loaded`) diff --git a/src/controls.ts b/src/controls.ts index 3d26f3d92..1034ab426 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -226,12 +226,10 @@ contro.on('release', ({ command }) => { // hard-coded keybindings -const hardcodedPressedKeys = new Set() -document.addEventListener('keydown', (e) => { - if (!isGameActive(false)) return - if (hardcodedPressedKeys.has('F3')) { - // reload chunks - if (e.code === 'KeyA') { +export const f3Keybinds = [ + { + key: 'KeyA', + action () { //@ts-expect-error const loadedChunks = Object.entries(worldView.loadedChunks).filter(([, v]) => v).map(([key]) => key.split(',').map(Number)) for (const [x, z] of loadedChunks) { @@ -242,12 +240,25 @@ document.addEventListener('keydown', (e) => { localServer.players[0].world.columns = {} } void reloadChunks() - } - if (e.code === 'KeyG') { - // todo make it work without reload + }, + mobileTitle: 'Reload chunks', + }, + { + key: 'KeyG', + action () { options.showChunkBorders = !options.showChunkBorders - void reloadChunks() - } + viewer.world.updateShowChunksBorder(options.showChunkBorders) + }, + mobileTitle: 'Toggle chunk borders', + } +] + +const hardcodedPressedKeys = new Set() +document.addEventListener('keydown', (e) => { + if (!isGameActive(false)) return + if (hardcodedPressedKeys.has('F3')) { + const keybind = f3Keybinds.find((v) => v.key === e.code) + if (keybind) keybind.action() return } @@ -388,6 +399,7 @@ const selectItem = async () => { } addEventListener('mousedown', async (e) => { + if ((e.target as HTMLElement).matches?.('#VRButton')) return void pointerLock.requestPointerLock() if (!bot) return // wheel click diff --git a/src/customClient.js b/src/customClient.js index 090a5c610..e349a8372 100644 --- a/src/customClient.js +++ b/src/customClient.js @@ -62,6 +62,7 @@ class CustomChannelClient extends EventEmitter { debug(params) } + this.emit('writePacket', name, params) customCommunication.sendData.call(this, { name, params, state: this.state }) } diff --git a/src/downloadAndOpenFile.ts b/src/downloadAndOpenFile.ts index 5efaedc12..b3d3a059c 100644 --- a/src/downloadAndOpenFile.ts +++ b/src/downloadAndOpenFile.ts @@ -3,7 +3,7 @@ import { openWorldZip } from './browserfs' import { getResourcePackName, installTexturePack, resourcePackState, updateTexturePackInstalledState } from './texturePack' import { setLoadingScreenStatus } from './utils' -const getFixedFilesize = (bytes: number) => { +export const getFixedFilesize = (bytes: number) => { return prettyBytes(bytes, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) } diff --git a/src/index.ts b/src/index.ts index f6deda939..948f4500f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -105,6 +105,11 @@ document.body.appendChild(renderer.domElement) // Create viewer const viewer: import('prismarine-viewer/viewer/lib/viewer').Viewer = new Viewer(renderer, options.numWorkers) window.viewer = viewer +Object.defineProperty(window, 'debugSceneChunks', { + get () { + return viewer.world.getLoadedChunksRelative(bot.entity.position) + }, +}) viewer.entities.entitiesOptions = { fontFamily: 'mojangles' } @@ -124,7 +129,7 @@ let previousWindowHeight = window.innerHeight const renderFrame = (time: DOMHighResTimeStamp) => { if (window.stopLoop) return window.requestAnimationFrame(renderFrame) - if (window.stopRender) return + if (window.stopRender || renderer.xr.isPresenting) return if (renderInterval) { delta += time - lastTime lastTime = time @@ -200,8 +205,8 @@ function hideCurrentScreens () { insertActiveModalStack('', []) } -const loadSingleplayer = (serverOverrides = {}) => { - void connect({ singleplayer: true, username: options.localUsername, password: '', serverOverrides }) +const loadSingleplayer = (serverOverrides = {}, flattenedServerOverrides = {}) => { + void connect({ singleplayer: true, username: options.localUsername, password: '', serverOverrides, serverOverridesFlat: flattenedServerOverrides }) } function listenGlobalEvents () { const menu = document.getElementById('play-screen') @@ -242,7 +247,7 @@ const cleanConnectIp = (host: string | undefined, defaultPort: string | undefine } async function connect (connectOptions: { - server?: string; singleplayer?: any; username: string; password?: any; proxy?: any; botVersion?: any; serverOverrides?; peerId?: string + server?: string; singleplayer?: any; username: string; password?: any; proxy?: any; botVersion?: any; serverOverrides?; serverOverridesFlat?; peerId?: string }) { document.getElementById('play-screen').style = 'display: none;' removePanorama() @@ -335,6 +340,7 @@ async function connect (connectOptions: { let localServer try { const serverOptions = _.defaultsDeep({}, connectOptions.serverOverrides ?? {}, options.localServerOptions, defaultServerOptions) + Object.assign(serverOptions, connectOptions.serverOverridesFlat ?? {}) const downloadMcData = async (version: string) => { setLoadingScreenStatus(`Downloading data for ${version}`) await loadScript(`./mc-data/${toMajorVersion(version)}.js`) @@ -535,7 +541,7 @@ async function connect (connectOptions: { window.debugMenu = debugMenu - void initVR(bot, renderer, viewer) + void initVR() postRenderFrameFn = () => { viewer.setFirstPersonCamera(null, bot.entity.yaw, bot.entity.pitch) @@ -577,6 +583,7 @@ async function connect (connectOptions: { function changeCallback () { notification.show = false + if (renderer.xr.isPresenting) return // todo if (!pointerLock.hasPointerLock && activeModalStack.length === 0) { showModal(pauseMenu) } @@ -693,7 +700,7 @@ watchValue(miscUiState, async s => { if (s.appLoaded) { // fs ready const qs = new URLSearchParams(window.location.search) if (qs.get('singleplayer') === '1') { - loadSingleplayer({ + loadSingleplayer({}, { worldFolder: undefined }) } @@ -711,19 +718,33 @@ watchValue(miscUiState, async s => { }) // #region fire click event on touch as we disable default behaviors -let activeTouch: { touch: Touch, elem: HTMLElement } | undefined +let activeTouch: { touch: Touch, elem: HTMLElement, start: number } | undefined document.body.addEventListener('touchend', (e) => { if (!isGameActive(true)) return if (activeTouch?.touch.identifier !== e.changedTouches[0].identifier) return - activeTouch.elem.click() + if (Date.now() - activeTouch.start > 500) { + activeTouch.elem.dispatchEvent(new Event('longtouch', { bubbles: true })) + } else { + activeTouch.elem.click() + } activeTouch = undefined }) document.body.addEventListener('touchstart', (e) => { if (!isGameActive(true)) return e.preventDefault() + let firstClickable // todo remove composedPath and this workaround when lit-element is fully dropped + const path = e.composedPath() as Array<{ click?: () => void }> + for (const elem of path) { + if (elem.click) { + firstClickable = elem + break + } + } + if (!firstClickable) return activeTouch = { touch: e.touches[0], - elem: e.composedPath()[0] as HTMLElement + elem: firstClickable, + start: Date.now(), } }, { passive: false }) // #endregion diff --git a/src/localServerMultiplayer.ts b/src/localServerMultiplayer.ts index a4f40e7c4..c7c2cd283 100644 --- a/src/localServerMultiplayer.ts +++ b/src/localServerMultiplayer.ts @@ -22,6 +22,9 @@ let peerInstance: Peer | undefined export const getJoinLink = () => { if (!peerInstance) return const url = new URL(window.location.href) + for (const key of url.searchParams.keys()) { + url.searchParams.delete(key) + } url.searchParams.set('connectPeer', peerInstance.id) url.searchParams.set('peerVersion', localServer!.options.version) return url.toString() diff --git a/src/menus/components/debug_overlay.js b/src/menus/components/debug_overlay.js index e169f6f06..6cee482a6 100644 --- a/src/menus/components/debug_overlay.js +++ b/src/menus/components/debug_overlay.js @@ -1,4 +1,8 @@ const { LitElement, html, css } = require('lit') +const { subscribeKey } = require('valtio/utils') +const { miscUiState } = require('../../globalState') +const { options } = require('../../optionsStorage') +const { getFixedFilesize } = require('../../downloadAndOpenFile') class DebugOverlay extends LitElement { static get styles () { @@ -55,7 +59,8 @@ class DebugOverlay extends LitElement { cursorBlock: { type: Object }, rendererDevice: { type: String }, bot: { type: Object }, - customEntries: { type: Object } + customEntries: { type: Object }, + packetsString: { type: String } } } @@ -63,6 +68,7 @@ class DebugOverlay extends LitElement { super() this.showOverlay = false this.customEntries = {} + this.packetsString = '' } firstUpdated () { @@ -72,6 +78,120 @@ class DebugOverlay extends LitElement { e.preventDefault() } }) + + let receivedTotal = 0 + let received = { + count: 0, + size: 0 + } + let sent = { + count: 0, + size: 0 + } + const packetsCountByNamePerSec = { + received: {}, + sent: {} + } + const hardcodedListOfDebugPacketsToIgnore = { + received: [ + 'entity_velocity', + 'sound_effect', + 'rel_entity_move', + 'entity_head_rotation', + 'entity_metadata', + 'entity_move_look', + 'teams', + 'entity_teleport', + 'entity_look', + 'ping', + 'entity_update_attributes', + 'player_info', + 'update_time', + 'animation', + 'entity_equipment', + 'entity_destroy', + 'named_entity_spawn', + 'update_light', + 'set_slot', + 'block_break_animation', + 'map_chunk', + 'spawn_entity', + 'world_particles', + 'keep_alive', + 'chat', + 'playerlist_header', + 'scoreboard_objective', + 'scoreboard_score' + ], + sent: [ + 'pong', + 'position', + 'look', + 'keep_alive', + 'position_look' + ] + } // todo cleanup? + const ignoredPackets = new Set('') + Object.defineProperty(window, 'debugTopPackets', { + get () { + return Object.fromEntries(Object.entries(packetsCountByName).map(([s, packets]) => [s, Object.fromEntries(Object.entries(packets).sort(([, n1], [, n2]) => { + return n2 - n1 + }))])) + } + }) + setInterval(() => { + this.packetsString = `↓ ${received.count} (${(received.size / 1024).toFixed(2)} KB/s, ${getFixedFilesize(receivedTotal)}) ↑ ${sent.count}` + received = { + count: 0, + size: 0 + } + sent = { + count: 0, + size: 0 + } + packetsCountByNamePerSec.received = {} + packetsCountByNamePerSec.sent = {} + }, 1000) + const packetsCountByName = { + received: {}, + sent: {} + } + + const managePackets = (type, name, data) => { + packetsCountByName[type][name] ??= 0 + packetsCountByName[type][name]++ + if (options.debugLogNotFrequentPackets && !ignoredPackets.has(name) && !hardcodedListOfDebugPacketsToIgnore[type].includes(name)) { + packetsCountByNamePerSec[type][name] ??= 0 + packetsCountByNamePerSec[type][name]++ + if (packetsCountByNamePerSec[type][name] > 5 || packetsCountByName[type][name] > 100) { // todo think of tracking the count within 10s + console.info(`[packet ${name} was ${type} too frequent] Ignoring...`) + ignoredPackets.add(name) + } else { + console.info(`[packet ${type}] ${name}`, /* ${JSON.stringify(data, null, 2)}` */ data) + } + } + } + + subscribeKey(miscUiState, 'gameLoaded', () => { + if (!miscUiState.gameLoaded) return + packetsCountByName.received = {} + packetsCountByName.sent = {} + const readPacket = (data, { name }, _buf, fullBuffer) => { + if (fullBuffer) { + const size = fullBuffer.byteLength + receivedTotal += size + received.size += size + } + received.count++ + managePackets('received', name, data) + } + bot._client.on('packet', readPacket) + bot._client.on('packet_name', (name, data) => readPacket(data, { name })) // custom client + bot._client.on('writePacket', (name, data) => { + sent.count++ + managePackets('sent', name, data) + }) + }) } updated (changedProperties) { @@ -128,6 +248,7 @@ class DebugOverlay extends LitElement {

XYZ: ${pos.x.toFixed(3)} / ${pos.y.toFixed(3)} / ${pos.z.toFixed(3)}

Chunk: ${Math.floor(pos.x % 16)} ~ ${Math.floor(pos.z % 16)} in ${Math.floor(pos.x / 16)} ~ ${Math.floor(pos.z / 16)}

+

Packets: ${this.packetsString}

Facing (viewer): ${rot[0].toFixed(3)} ${rot[1].toFixed(3)}

Facing (minecraft): ${quadsDescription[minecraftQuad]} (${minecraftYaw.toFixed(1)} ${(rot[1] * -180 / Math.PI).toFixed(1)})

Light: ${skyL} (${skyL} sky)

diff --git a/src/menus/hud.js b/src/menus/hud.js index 4a2296957..b82aca3f5 100644 --- a/src/menus/hud.js +++ b/src/menus/hud.js @@ -1,3 +1,6 @@ +import { f3Keybinds } from '../controls' +import { showOptionsModal } from '../react/SelectOption' + const { LitElement, html, css, unsafeCSS } = require('lit') const { showModal, miscUiState } = require('../globalState') const { options, watchValue } = require('../optionsStorage') @@ -219,7 +222,12 @@ class Hud extends LitElement {
{ window.dispatchEvent(new MouseEvent('mousedown', { button: 1 })) }}>S
-
{ +
{ + const select = await showOptionsModal('', f3Keybinds.filter(f3Keybind => f3Keybind.mobileTitle).map(f3Keybind => f3Keybind.mobileTitle)) + if (!select) return + const f3Keybind = f3Keybinds.find(f3Keybind => f3Keybind.mobileTitle === select) + f3Keybind.action() + }} @pointerdown=${(e) => { this.shadowRoot.getElementById('debug-overlay').showOverlay = !this.shadowRoot.getElementById('debug-overlay').showOverlay }}>F3
{ diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx index 4788057db..5ee73a839 100644 --- a/src/optionsGuiScheme.tsx +++ b/src/optionsGuiScheme.tsx @@ -123,6 +123,11 @@ export const guiOptionsScheme: { custom () { return