From 4248b38e86b873ec3bfcaa8d16d971bc3a0c50c4 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sun, 5 Nov 2023 07:53:00 +0300 Subject: [PATCH 01/21] hotfix: don't crash singleplayer screen when save has no LevelName --- pnpm-lock.yaml | 6 +++--- src/react/SingleplayerProvider.tsx | 15 ++++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 680a76bcf..3fdc74ce2 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/4900e850877350df5d223555a3c02fce3959eeb9 + version: github.com/zardoy/space-squid/a639714c26e2252b34be833f64f23d9f45f136de fs-extra: specifier: ^11.1.1 version: 11.1.1 @@ -14750,8 +14750,8 @@ packages: - utf-8-validate dev: false - github.com/zardoy/space-squid/4900e850877350df5d223555a3c02fce3959eeb9: - resolution: {tarball: https://codeload.github.com/zardoy/space-squid/tar.gz/4900e850877350df5d223555a3c02fce3959eeb9} + github.com/zardoy/space-squid/a639714c26e2252b34be833f64f23d9f45f136de: + resolution: {tarball: https://codeload.github.com/zardoy/space-squid/tar.gz/a639714c26e2252b34be833f64f23d9f45f136de} name: flying-squid version: 0.0.0-dev engines: {node: '>=8'} diff --git a/src/react/SingleplayerProvider.tsx b/src/react/SingleplayerProvider.tsx index 497e0b007..bb9e34da1 100644 --- a/src/react/SingleplayerProvider.tsx +++ b/src/react/SingleplayerProvider.tsx @@ -16,19 +16,20 @@ export const readWorlds = () => { (async () => { try { const worlds = await fs.promises.readdir(`/data/worlds`) - worldsProxy.value = (await Promise.allSettled(worlds.map(async (world) => { - const { levelDat } = (await readLevelDat(`/data/worlds/${world}`))! + worldsProxy.value = (await Promise.allSettled(worlds.map(async (folder) => { + const { levelDat } = (await readLevelDat(`/data/worlds/${folder}`))! let size = 0 // todo use whole dir size - for (const region of await fs.promises.readdir(`/data/worlds/${world}/region`)) { - const stat = await fs.promises.stat(`/data/worlds/${world}/region/${region}`) + for (const region of await fs.promises.readdir(`/data/worlds/${folder}/region`)) { + const stat = await fs.promises.stat(`/data/worlds/${folder}/region/${region}`) size += stat.size } + const levelName = levelDat.LevelName as string | undefined return { - name: world, - title: levelDat.LevelName, + name: folder, + title: levelName ?? folder, lastPlayed: levelDat.LastPlayed && longArrayToNumber(levelDat.LastPlayed), - detail: `${levelDat.Version?.Name ?? 'unknown version'}, ${world}`, + detail: `${levelDat.Version?.Name ?? 'unknown version'}, ${folder}`, size, } satisfies WorldProps }))).filter(x => { From 3b4d5e04b8c0344397405ffbb831d74e7eb00e4a Mon Sep 17 00:00:00 2001 From: Vitaly Date: Fri, 10 Nov 2023 11:13:58 +0300 Subject: [PATCH 02/21] fix: minimal value of settings by default is now 0 which is important for sound fix: don't start audiocontext when muted, so browser don't try to take exclusive control over sound output device when it's busy in super advanced setups --- src/basicSounds.ts | 6 ++++-- src/react/Slider.tsx | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) 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/react/Slider.tsx b/src/react/Slider.tsx index b78d3cf4f..bc6c5890a 100644 --- a/src/react/Slider.tsx +++ b/src/react/Slider.tsx @@ -22,7 +22,7 @@ const Slider: React.FC = ({ width, value: valueProp, valueDisplay, - min = 1, + min = 0, max = 100, disabledReason, From df6ed989d09b6adc2ed321e826764cb581263fed Mon Sep 17 00:00:00 2001 From: Vitaly Date: Fri, 10 Nov 2023 12:21:19 +0300 Subject: [PATCH 03/21] feat: add `debugSceneChunks` and `debugChangedOptions` global variables that available via browser console fix: reset all options sometimes didn't work (don't mutate default options) --- README.MD | 4 +++- prismarine-viewer/viewer/lib/worldrenderer.ts | 11 +++++++++++ src/index.ts | 5 +++++ src/optionsStorage.ts | 15 ++++++++++----- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/README.MD b/README.MD index 94c4987ce..8297d2c0e 100644 --- a/README.MD +++ b/README.MD @@ -53,11 +53,13 @@ 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. - `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. diff --git a/prismarine-viewer/viewer/lib/worldrenderer.ts b/prismarine-viewer/viewer/lib/worldrenderer.ts index 79c7265da..d7cea5e9f 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 @@ -33,6 +34,7 @@ export class WorldRenderer { workers: any[] = [] texturesVersion?: string + constructor (public scene: THREE.Scene, numWorkers = 4) { // init workers for (let i = 0; i < numWorkers; i++) { @@ -182,6 +184,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/src/index.ts b/src/index.ts index f6deda939..a22800d37 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' } diff --git a/src/optionsStorage.ts b/src/optionsStorage.ts index 986e17e79..f186072ae 100644 --- a/src/optionsStorage.ts +++ b/src/optionsStorage.ts @@ -4,8 +4,6 @@ import { proxy, subscribe } from 'valtio/vanilla' // weird webpack configuration bug: it cant import valtio/utils in this file import { subscribeKey } from 'valtio/utils' -const mergeAny: (arg1: T, arg2: any) => T = Object.assign - const defaultOptions = { renderDistance: 2, multiplayerRenderDistance: 2, @@ -53,9 +51,10 @@ const defaultOptions = { export type AppOptions = typeof defaultOptions -export const options = proxy( - mergeAny(defaultOptions, JSON.parse(localStorage.options || '{}')) -) +export const options: AppOptions = proxy({ + ...defaultOptions, + ...JSON.parse(localStorage.options || '{}') +}) window.options = window.settings = options @@ -63,6 +62,12 @@ export const resetOptions = () => { Object.assign(options, defaultOptions) } +Object.defineProperty(window, 'debugChangedOptions', { + get () { + return Object.fromEntries(Object.entries(options).filter(([key, v]) => defaultOptions[key] !== v)) + }, +}) + subscribe(options, () => { localStorage.options = JSON.stringify(options) }) From 2bd33071996a7ea27ac2bcc891e457e8a12f69ce Mon Sep 17 00:00:00 2001 From: Vitaly Date: Fri, 10 Nov 2023 12:25:39 +0300 Subject: [PATCH 04/21] fix: normal mode was unplayable when webxr was available fix: in vr mode performance was degraded since every frame was rendered twice fix: enter vr is now displayed propertly on mobile devices --- prismarine-viewer/viewer/lib/viewer.ts | 6 ++++-- src/index.ts | 4 ++-- src/styles.css | 6 ++++++ src/vr.js | 23 +++++++++++------------ 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/prismarine-viewer/viewer/lib/viewer.ts b/prismarine-viewer/viewer/lib/viewer.ts index 3ff8e6caa..fcb64627b 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 diff --git a/src/index.ts b/src/index.ts index a22800d37..a59fbc106 100644 --- a/src/index.ts +++ b/src/index.ts @@ -129,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 @@ -540,7 +540,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) diff --git a/src/styles.css b/src/styles.css index a4034529b..d19a6ada2 100644 --- a/src/styles.css +++ b/src/styles.css @@ -64,6 +64,12 @@ body { text-shadow: 1px 1px #222; } +#VRButton { + background: rgba(0, 0, 0, 0.3) !important; + opacity: 0.7 !important; + position: fixed !important; +} + .dirt-bg { position: absolute; top: 0; diff --git a/src/vr.js b/src/vr.js index 72d23cb13..7e11cb71c 100644 --- a/src/vr.js +++ b/src/vr.js @@ -1,13 +1,11 @@ -/* global THREE */ - const { VRButton } = require('three/examples/jsm/webxr/VRButton.js') const { GLTFLoader } = require('three/examples/jsm/loaders/GLTFLoader.js') const { XRControllerModelFactory } = require('three/examples/jsm/webxr/XRControllerModelFactory.js') -const TWEEN = require('@tweenjs/tween.js') -async function initVR (bot, renderer, viewer) { +async function initVR () { + const { renderer } = viewer if (!('xr' in navigator)) return - const isSupported = await navigator.xr.isSessionSupported('immersive-vr') + const isSupported = await navigator.xr.isSessionSupported('immersive-vr') && !!XRSession.prototype.updateRenderState // e.g. android webview doesn't support updateRenderState if (!isSupported) return // VR @@ -34,14 +32,10 @@ async function initVR (bot, renderer, viewer) { }) controller2.add(hand2) - viewer.setFirstPersonCamera = function (pos, yaw, pitch) { - if (pos) new TWEEN.Tween(user.position).to({ x: pos.x, y: pos.y, z: pos.z }, 50).start() - user.rotation.set(pitch, yaw, 0, 'ZYX') - } - let rotSnapReset = true let yawOffset = 0 renderer.setAnimationLoop(() => { + if (!renderer.xr.isPresenting) return if (hand1.xrInputSource && hand2.xrInputSource) { hand1.xAxis = hand1.xrInputSource.gamepad.axes[2] hand1.yAxis = hand1.xrInputSource.gamepad.axes[3] @@ -76,9 +70,14 @@ async function initVR (bot, renderer, viewer) { bot.setControlState('right', hand2.xAxis < -0.5) bot.setControlState('left', hand2.xAxis > 0.5) - TWEEN.update() viewer.update() - renderer.render(viewer.scene, viewer.camera) + viewer.render() + }) + renderer.xr.addEventListener('sessionstart', () => { + viewer.cameraObjectOverride = user + }) + renderer.xr.addEventListener('sessionend', () => { + viewer.cameraObjectOverride = undefined }) } From c36d42e5d59b6f6ed40c2f68bc9f994458a7e24c Mon Sep 17 00:00:00 2001 From: Vitaly Date: Fri, 10 Nov 2023 12:48:55 +0300 Subject: [PATCH 05/21] feat: display fullscreen button on android devices --- src/index.ts | 11 ++++++++++- src/panorama.ts | 2 +- src/react/EnterFullscreenButton.tsx | 28 ++++++++++++++++++++++++++++ src/reactUi.jsx | 2 ++ 4 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 src/react/EnterFullscreenButton.tsx diff --git a/src/index.ts b/src/index.ts index a59fbc106..cafaca35a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -726,9 +726,18 @@ document.body.addEventListener('touchend', (e) => { 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 } }, { passive: false }) // #endregion diff --git a/src/panorama.ts b/src/panorama.ts index b835a7af6..896759098 100644 --- a/src/panorama.ts +++ b/src/panorama.ts @@ -117,10 +117,10 @@ export async function addPanoramaCubeMap () { } export function removePanorama () { + shouldDisplayPanorama = false if (!panoramaCubeMap) return viewer.camera = new THREE.PerspectiveCamera(options.fov, window.innerWidth / window.innerHeight, 0.1, 1000) viewer.camera.updateProjectionMatrix() viewer.scene.remove(panoramaCubeMap) panoramaCubeMap = null - shouldDisplayPanorama = false } diff --git a/src/react/EnterFullscreenButton.tsx b/src/react/EnterFullscreenButton.tsx new file mode 100644 index 000000000..7a4562e3a --- /dev/null +++ b/src/react/EnterFullscreenButton.tsx @@ -0,0 +1,28 @@ +import { useUsingTouch } from '@dimaka/interface' +import { useEffect, useState } from 'react' +import Button from './Button' + +export default () => { + const [fullScreen, setFullScreen] = useState(false) + useEffect(() => { + document.documentElement.addEventListener('fullscreenchange', () => { + setFullScreen(!!document.fullscreenElement) + }) + }, []) + + const usingTouch = useUsingTouch() + if (!usingTouch || !document.documentElement.requestFullscreen || fullScreen) return null + + return