From 972625757792641490a01efa12f59edfaa314fb8 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Thu, 22 May 2025 14:46:44 +0300 Subject: [PATCH 001/181] fix: do not display capture lock message when possilbe (avoid flickering - do strategy switch) feat: make tab (players list) keybindign configurable and add a way to assign to a gamepad button --- package.json | 2 +- pnpm-lock.yaml | 16 +++++++------- src/controls.ts | 5 +++++ src/react/GlobalOverlayHints.tsx | 2 +- src/react/PlayerListOverlayProvider.tsx | 29 ++++++------------------- src/utils.ts | 9 ++++---- 6 files changed, 27 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index 7d3a5011..655cd394 100644 --- a/package.json +++ b/package.json @@ -140,7 +140,7 @@ "browserify-zlib": "^0.2.0", "buffer": "^6.0.3", "constants-browserify": "^1.0.0", - "contro-max": "^0.1.8", + "contro-max": "^0.1.9", "crypto-browserify": "^3.12.0", "cypress-esbuild-preprocessor": "^1.0.2", "eslint": "^8.50.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c9be7ee5..60086040 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -304,8 +304,8 @@ importers: specifier: ^1.0.0 version: 1.0.0 contro-max: - specifier: ^0.1.8 - version: 0.1.8(typescript@5.5.4) + specifier: ^0.1.9 + version: 0.1.9(typescript@5.5.4) crypto-browserify: specifier: ^3.12.0 version: 3.12.1 @@ -4237,8 +4237,8 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} - contro-max@0.1.8: - resolution: {integrity: sha512-5SoeudO8Zzfj/gbFTDrMRFJny02+MY1lBtb2NyCNiBLtHAfvhWZxZs/Z3yJvKL2rY/qKUZs9gTQOIDygBcBrdw==} + contro-max@0.1.9: + resolution: {integrity: sha512-zH9FB60EzhHKublD92d11QuarYRTdYci5rvDgwDr5XXwUqae5mr6IgzXGcr78T2odnO/Aeqmrf32RDwJIl5GfQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} convert-source-map@1.9.0: @@ -9751,7 +9751,7 @@ snapshots: '@babel/core': 7.26.9 '@babel/helper-compilation-targets': 7.26.5 '@babel/helper-plugin-utils': 7.26.5 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 lodash.debounce: 4.0.8 resolve: 1.22.10 transitivePeerDependencies: @@ -13322,7 +13322,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 transitivePeerDependencies: - supports-color optional: true @@ -14225,7 +14225,7 @@ snapshots: content-type@1.0.5: {} - contro-max@0.1.8(typescript@5.5.4): + contro-max@0.1.9(typescript@5.5.4): dependencies: events: 3.3.0 lodash-es: 4.17.21 @@ -17268,7 +17268,7 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.12 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 decode-named-character-reference: 1.1.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 diff --git a/src/controls.ts b/src/controls.ts index f8160cfd..9b2c3cbf 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -27,6 +27,7 @@ import { onCameraMove, onControInit } from './cameraRotationControls' import { createNotificationProgressReporter } from './core/progressReporter' import { appStorage } from './react/appStorageProvider' import { switchGameMode } from './packetsReplay/replayPackets' +import { tabListState } from './react/PlayerListOverlayProvider' export const customKeymaps = proxy(appStorage.keybindings) @@ -65,6 +66,7 @@ export const contro = new ControMax({ // client side zoom: ['KeyC'], viewerConsole: ['Backquote'], + playersList: ['Tab'], }, ui: { toggleFullscreen: ['F11'], @@ -405,6 +407,9 @@ const onTriggerOrReleased = (command: Command, pressed: boolean) => { case 'general.rotateCameraDown': cameraRotationControls.handleCommand(command, pressed) break + case 'general.playersList': + tabListState.isOpen = pressed + break } } } diff --git a/src/react/GlobalOverlayHints.tsx b/src/react/GlobalOverlayHints.tsx index aa340953..ddbb1116 100644 --- a/src/react/GlobalOverlayHints.tsx +++ b/src/react/GlobalOverlayHints.tsx @@ -6,7 +6,7 @@ import PixelartIcon, { pixelartIcons } from './PixelartIcon' import { useUsingTouch } from './utilsApp' export const displayHintsState = proxy({ - captureMouseHint: true + captureMouseHint: false }) export default () => { diff --git a/src/react/PlayerListOverlayProvider.tsx b/src/react/PlayerListOverlayProvider.tsx index 478fd1d2..3ff69c41 100644 --- a/src/react/PlayerListOverlayProvider.tsx +++ b/src/react/PlayerListOverlayProvider.tsx @@ -1,4 +1,4 @@ -import { useSnapshot } from 'valtio' +import { proxy, useSnapshot } from 'valtio' import { useState, useEffect, useMemo } from 'react' import { isGameActive } from '../globalState' import PlayerListOverlay from './PlayerListOverlay' @@ -9,28 +9,18 @@ const MAX_ROWS_PER_COL = 10 type Players = typeof bot.players +export const tabListState = proxy({ + isOpen: false, +}) + export default () => { + const { isOpen } = useSnapshot(tabListState) + const serverIp = lastConnectOptions.value?.server const [clientId, setClientId] = useState(bot._client.uuid) const [players, setPlayers] = useState({}) - const [isOpen, setIsOpen] = useState(false) const [counter, setCounter] = useState(0) - const handleKeyDown = (e) => { - if (!isGameActive(true)) return - if (e.key === 'Tab') { - setIsOpen(prev => true) - e.preventDefault() - } - } - - const handleKeyUp = (e) => { - if (e.key === 'Tab') { - setIsOpen(prev => false) - e.preventDefault() - } - } - useEffect(() => { function requestUpdate () { setPlayers(bot?.players ?? {}) @@ -58,15 +48,10 @@ export default () => { }) } - document.addEventListener('keydown', handleKeyDown) - document.addEventListener('keyup', handleKeyUp) - const playerlistHeader = () => setCounter(prev => prev + 1) bot._client.on('playerlist_header', playerlistHeader) return () => { - document.removeEventListener('keydown', handleKeyDown) - document.removeEventListener('keyup', handleKeyUp) bot?._client.removeListener('playerlist_header', playerlistHeader) } }, [serverIp]) diff --git a/src/utils.ts b/src/utils.ts index 8ea6d3aa..d48fbbc3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -39,14 +39,14 @@ export const pointerLock = { if (options.autoFullScreen) { void goFullscreen() } - const displayBrowserProblem = () => { + const displayMouseCaptureFailure = () => { // if (notificationProxy.id === 'auto-login') return // prevent notification hide // showNotification('Browser Delay Limitation', navigator['keyboard'] ? 'Click on screen, enable Auto Fullscreen or F11' : 'Click on screen or use fullscreen in Chrome') // notificationProxy.id = 'pointerlockchange' displayHintsState.captureMouseHint = true } if (!(document.fullscreenElement && navigator['keyboard']) && this.justHitEscape) { - displayBrowserProblem() + displayMouseCaptureFailure() } else { //@ts-expect-error const promise: any = document.documentElement.requestPointerLock({ @@ -58,9 +58,10 @@ export const pointerLock = { document.documentElement.requestPointerLock() } else if (error.name === 'SecurityError') { // cause: https://discourse.threejs.org/t/how-to-avoid-pointerlockcontrols-error/33017/4 - displayBrowserProblem() + displayMouseCaptureFailure() } else { - console.error(error) + displayMouseCaptureFailure() + console.warn('Failed to request pointer lock:', error) } }) } From 3cd778538c52fe6327b0d031b026190681e5f1f9 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Thu, 22 May 2025 14:50:58 +0300 Subject: [PATCH 002/181] feat: Sync armor rotation for players (#363) --- renderer/viewer/three/entities.ts | 86 +++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/renderer/viewer/three/entities.ts b/renderer/viewer/three/entities.ts index 320daff9..1a5e7c3f 100644 --- a/renderer/viewer/three/entities.ts +++ b/renderer/viewer/three/entities.ts @@ -292,6 +292,9 @@ export class Entities { playerObject.animation.update(playerObject, dt) } + // Update armor positions + this.syncArmorPositions(entity) + // Update visibility based on distance and chunk load status if (botPos && entity.position) { const dx = entity.position.x - botPos.x @@ -312,6 +315,79 @@ export class Entities { } } + private syncArmorPositions (entity: SceneEntity) { + if (!entity.playerObject) return + + // todo-low use property access for less loop iterations (small performance gain) + entity.traverse((armor) => { + if (!armor.name.startsWith('geometry_armor_')) return + + const { skin } = entity.playerObject! + + switch (armor.name) { + case 'geometry_armor_head': + // Head armor sync + if (armor.children[0]?.children[0]) { + armor.children[0].children[0].rotation.set( + -skin.head.rotation.x, + skin.head.rotation.y, + skin.head.rotation.z, + skin.head.rotation.order + ) + } + break + + case 'geometry_armor_legs': + // Legs armor sync + if (armor.children[0]) { + // Left leg + if (armor.children[0].children[2]) { + armor.children[0].children[2].rotation.set( + -skin.leftLeg.rotation.x, + skin.leftLeg.rotation.y, + skin.leftLeg.rotation.z, + skin.leftLeg.rotation.order + ) + } + // Right leg + if (armor.children[0].children[1]) { + armor.children[0].children[1].rotation.set( + -skin.rightLeg.rotation.x, + skin.rightLeg.rotation.y, + skin.rightLeg.rotation.z, + skin.rightLeg.rotation.order + ) + } + } + break + + case 'geometry_armor_feet': + // Boots armor sync + if (armor.children[0]) { + // Right boot + if (armor.children[0].children[0]) { + armor.children[0].children[0].rotation.set( + -skin.rightLeg.rotation.x, + skin.rightLeg.rotation.y, + skin.rightLeg.rotation.z, + skin.rightLeg.rotation.order + ) + } + // Left boot (reversed Z rotation) + if (armor.children[0].children[1]) { + armor.children[0].children[1].rotation.set( + -skin.leftLeg.rotation.x, + skin.leftLeg.rotation.y, + -skin.leftLeg.rotation.z, + skin.leftLeg.rotation.order + ) + } + } + break + } + }) + } + getPlayerObject (entityId: string | number) { const playerObject = this.entities[entityId]?.playerObject return playerObject @@ -1203,6 +1279,16 @@ function addArmorModel (worldRenderer: WorldRendererThree, entityMesh: THREE.Obj }) } else { mesh = getMesh(worldRenderer, texturePath, armorModel[slotType]) + // // enable debug mode to see the mesh + // mesh.traverse(c => { + // if (c instanceof THREE.Mesh) { + // c.material.wireframe = true + // } + // }) + if (slotType === 'head') { + // avoid z-fighting with the head + mesh.children[0].position.y += 0.01 + } mesh.name = meshName material = mesh.material if (!isPlayerHead) { From 04a85e9bd19db2a626d6ec6157f10d7056eb0b2c Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sat, 24 May 2025 18:26:47 +0300 Subject: [PATCH 003/181] minimap: don't do more 20 updates per seconds --- src/controls.ts | 1 + src/react/Minimap.tsx | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/src/controls.ts b/src/controls.ts index 9b2c3cbf..d1e9b1e4 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -499,6 +499,7 @@ contro.on('trigger', ({ command }) => { case 'general.rotateCameraRight': case 'general.rotateCameraUp': case 'general.rotateCameraDown': + case 'general.playersList': // no-op break case 'general.swapHands': { diff --git a/src/react/Minimap.tsx b/src/react/Minimap.tsx index 25ee59ed..9eb8a253 100644 --- a/src/react/Minimap.tsx +++ b/src/react/Minimap.tsx @@ -22,8 +22,14 @@ export default ( const canvasTick = useRef(0) const canvasRef = useRef(null) const [position, setPosition] = useState({ x: 0, y: 0, z: 0 }) + const lastUpdate = useRef(0) + const THROTTLE_MS = 50 // 20fps const updateMap = () => { + const now = Date.now() + if (now - lastUpdate.current < THROTTLE_MS) return + lastUpdate.current = now + setPosition({ x: adapter.playerPosition.x, y: adapter.playerPosition.y, z: adapter.playerPosition.z }) if (adapter.mapDrawer) { if (!full.current) { From 0c68e63ba619fcd39a9dbb9eb8fe8cdf689c4282 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sun, 25 May 2025 12:55:27 +0300 Subject: [PATCH 004/181] fix: restore VR support. Fix rotation / position camera bugs --- renderer/viewer/lib/worldrendererCommon.ts | 1 + renderer/viewer/three/cameraShake.ts | 24 ++++++++++------ renderer/viewer/three/documentRenderer.ts | 32 ++++++++++++--------- renderer/viewer/three/graphicsBackend.ts | 3 ++ renderer/viewer/three/world/vr.ts | 17 ++++++----- renderer/viewer/three/worldrendererThree.ts | 30 ++++++++++--------- src/appViewer.ts | 2 +- src/optionsGuiScheme.tsx | 6 +++- src/optionsStorage.ts | 1 + src/watchOptions.ts | 5 ++++ 10 files changed, 76 insertions(+), 45 deletions(-) diff --git a/renderer/viewer/lib/worldrendererCommon.ts b/renderer/viewer/lib/worldrendererCommon.ts index ef82f7af..26e3bf2e 100644 --- a/renderer/viewer/lib/worldrendererCommon.ts +++ b/renderer/viewer/lib/worldrendererCommon.ts @@ -43,6 +43,7 @@ export const defaultWorldRendererConfig = { starfield: true, addChunksBatchWaitTime: 200, vrSupport: true, + vrPageGameRendering: true, renderEntities: true, fov: 75, fetchPlayerSkins: true, diff --git a/renderer/viewer/three/cameraShake.ts b/renderer/viewer/three/cameraShake.ts index f6a61e2e..6fe483cc 100644 --- a/renderer/viewer/three/cameraShake.ts +++ b/renderer/viewer/three/cameraShake.ts @@ -1,4 +1,5 @@ import * as THREE from 'three' +import { WorldRendererThree } from './worldrendererThree' export class CameraShake { private rollAngle = 0 @@ -8,7 +9,7 @@ export class CameraShake { private basePitch = 0 private baseYaw = 0 - constructor (public camera: THREE.Camera, public onRenderCallbacks: Array<() => void>) { + constructor (public worldRenderer: WorldRendererThree, public onRenderCallbacks: Array<() => void>) { onRenderCallbacks.push(() => { this.update() }) @@ -62,14 +63,21 @@ export class CameraShake { } } - // Create rotation quaternions - const pitchQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), this.basePitch) - const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), this.baseYaw) - const rollQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), THREE.MathUtils.degToRad(this.rollAngle)) + const camera = this.worldRenderer.cameraGroupVr || this.worldRenderer.camera - // Combine rotations in the correct order: pitch -> yaw -> roll - const finalQuat = yawQuat.multiply(pitchQuat).multiply(rollQuat) - this.camera.setRotationFromQuaternion(finalQuat) + if (this.worldRenderer.cameraGroupVr) { + // For VR camera, only apply yaw rotation + const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), this.baseYaw) + camera.setRotationFromQuaternion(yawQuat) + } else { + // For regular camera, apply all rotations + const pitchQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), this.basePitch) + const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), this.baseYaw) + const rollQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), THREE.MathUtils.degToRad(this.rollAngle)) + // Combine rotations in the correct order: pitch -> yaw -> roll + const finalQuat = yawQuat.multiply(pitchQuat).multiply(rollQuat) + camera.setRotationFromQuaternion(finalQuat) + } } private easeOut (t: number): number { diff --git a/renderer/viewer/three/documentRenderer.ts b/renderer/viewer/three/documentRenderer.ts index 46672b74..1d556c2a 100644 --- a/renderer/viewer/three/documentRenderer.ts +++ b/renderer/viewer/three/documentRenderer.ts @@ -3,6 +3,7 @@ import Stats from 'stats.js' import StatsGl from 'stats-gl' import * as tween from '@tweenjs/tween.js' import { GraphicsBackendConfig, GraphicsInitOptions } from '../../../src/appViewer' +import { WorldRendererConfig } from '../lib/worldrendererCommon' export class DocumentRenderer { readonly canvas = document.createElement('canvas') @@ -23,6 +24,7 @@ export class DocumentRenderer { droppedFpsPercentage: number config: GraphicsBackendConfig onRender = [] as Array<(sizeChanged: boolean) => void> + inWorldRenderingConfig: WorldRendererConfig | undefined constructor (initOptions: GraphicsInitOptions) { this.config = initOptions.config @@ -94,7 +96,7 @@ export class DocumentRenderer { if (this.disconnected) return this.animationFrameId = requestAnimationFrame(animate) - if (this.paused) return + if (this.paused || (this.renderer.xr.isPresenting && !this.inWorldRenderingConfig?.vrPageGameRendering)) return // Handle FPS limiting if (this.config.fpsLimit) { @@ -117,18 +119,7 @@ export class DocumentRenderer { sizeChanged = true } - this.preRender() - this.stats.markStart() - tween.update() - if (!window.freezeRender) { - this.render(sizeChanged) - } - for (const fn of this.onRender) { - fn(sizeChanged) - } - this.renderedFps++ - this.stats.markEnd() - this.postRender() + this.frameRender(sizeChanged) // Update stats visibility each frame if (this.config.statsVisible !== undefined) { @@ -139,6 +130,21 @@ export class DocumentRenderer { animate() } + frameRender (sizeChanged: boolean) { + this.preRender() + this.stats.markStart() + tween.update() + if (!window.freezeRender) { + this.render(sizeChanged) + } + for (const fn of this.onRender) { + fn(sizeChanged) + } + this.renderedFps++ + this.stats.markEnd() + this.postRender() + } + setPaused (paused: boolean) { this.paused = paused } diff --git a/renderer/viewer/three/graphicsBackend.ts b/renderer/viewer/three/graphicsBackend.ts index 6ac068d7..37acfde9 100644 --- a/renderer/viewer/three/graphicsBackend.ts +++ b/renderer/viewer/three/graphicsBackend.ts @@ -8,6 +8,7 @@ import supportedVersions from '../../../src/supportedVersions.mjs' import { WorldRendererThree } from './worldrendererThree' import { DocumentRenderer } from './documentRenderer' import { PanoramaRenderer } from './panorama' +import { initVR } from './world/vr' // https://discourse.threejs.org/t/updates-to-color-management-in-three-js-r152/50791 THREE.ColorManagement.enabled = false @@ -87,10 +88,12 @@ const createGraphicsBackend: GraphicsBackendLoader = (initOptions: GraphicsInitO panoramaRenderer = null } worldRenderer = new WorldRendererThree(documentRenderer.renderer, initOptions, displayOptions) + void initVR(worldRenderer, documentRenderer) await worldRenderer.worldReadyPromise documentRenderer.render = (sizeChanged: boolean) => { worldRenderer?.render(sizeChanged) } + documentRenderer.inWorldRenderingConfig = displayOptions.inWorldRenderingConfig window.world = worldRenderer callModsMethod('worldReady', worldRenderer) } diff --git a/renderer/viewer/three/world/vr.ts b/renderer/viewer/three/world/vr.ts index 925ba0bb..c2665585 100644 --- a/renderer/viewer/three/world/vr.ts +++ b/renderer/viewer/three/world/vr.ts @@ -4,8 +4,9 @@ import { XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerM import { buttonMap as standardButtonsMap } from 'contro-max/build/gamepad' import * as THREE from 'three' import { WorldRendererThree } from '../worldrendererThree' +import { DocumentRenderer } from '../documentRenderer' -export async function initVR (worldRenderer: WorldRendererThree) { +export async function initVR (worldRenderer: WorldRendererThree, documentRenderer: DocumentRenderer) { if (!('xr' in navigator) || !worldRenderer.worldRendererConfig.vrSupport) return const { renderer } = worldRenderer @@ -26,12 +27,13 @@ export async function initVR (worldRenderer: WorldRendererThree) { function enableVr () { renderer.xr.enabled = true + // renderer.xr.setReferenceSpaceType('local-floor') worldRenderer.reactiveState.preventEscapeMenu = true } function disableVr () { renderer.xr.enabled = false - worldRenderer.cameraObjectOverride = undefined + worldRenderer.cameraGroupVr = undefined worldRenderer.reactiveState.preventEscapeMenu = false worldRenderer.scene.remove(user) vrButtonContainer.hidden = true @@ -189,7 +191,7 @@ export async function initVR (worldRenderer: WorldRendererThree) { } // appViewer.backend?.updateCamera(null, yawOffset, 0) - worldRenderer.updateCamera(null, bot.entity.yaw, bot.entity.pitch) + // worldRenderer.updateCamera(null, bot.entity.yaw, bot.entity.pitch) // todo restore this logic (need to preserve ability to move camera) // const xrCamera = renderer.xr.getCamera() @@ -197,16 +199,13 @@ export async function initVR (worldRenderer: WorldRendererThree) { // bot.entity.yaw = Math.atan2(-d.x, -d.z) // bot.entity.pitch = Math.asin(d.y) - // todo ? - // bot.physics.stepHeight = 1 - - worldRenderer.render() + documentRenderer.frameRender(false) }) renderer.xr.addEventListener('sessionstart', () => { - worldRenderer.cameraObjectOverride = user + worldRenderer.cameraGroupVr = user }) renderer.xr.addEventListener('sessionend', () => { - worldRenderer.cameraObjectOverride = undefined + worldRenderer.cameraGroupVr = undefined }) worldRenderer.abortController.signal.addEventListener('abort', disableVr) diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index 5c16aa9a..f8641ab8 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -20,7 +20,6 @@ import { armorModel } from './entity/armorModels' import { disposeObject } from './threeJsUtils' import { CursorBlock } from './world/cursorBlock' import { getItemUv } from './appShared' -import { initVR } from './world/vr' import { Entities } from './entities' import { ThreeJsSound } from './threeJsSound' import { CameraShake } from './cameraShake' @@ -42,7 +41,7 @@ export class WorldRendererThree extends WorldRendererCommon { ambientLight = new THREE.AmbientLight(0xcc_cc_cc) directionalLight = new THREE.DirectionalLight(0xff_ff_ff, 0.5) entities = new Entities(this) - cameraObjectOverride?: THREE.Object3D // for xr + cameraGroupVr?: THREE.Object3D material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.1 }) itemsTexture: THREE.Texture cursorBlock = new CursorBlock(this) @@ -91,10 +90,9 @@ export class WorldRendererThree extends WorldRendererCommon { this.addDebugOverlay() this.resetScene() void this.init() - void initVR(this) this.soundSystem = new ThreeJsSound(this) - this.cameraShake = new CameraShake(this.camera, this.onRender) + this.cameraShake = new CameraShake(this, this.onRender) this.media = new ThreeJsMedia(this) // this.fountain = new Fountain(this.scene, this.scene, { // position: new THREE.Vector3(0, 10, 0), @@ -106,6 +104,10 @@ export class WorldRendererThree extends WorldRendererCommon { this.worldSwitchActions() } + get cameraObject () { + return this.cameraGroupVr || this.camera + } + worldSwitchActions () { this.onWorldSwitched.push(() => { // clear custom blocks @@ -301,7 +303,7 @@ export class WorldRendererThree extends WorldRendererCommon { updateViewerPosition (pos: Vec3): void { this.viewerPosition = pos - const cameraPos = this.camera.position.toArray().map(x => Math.floor(x / 16)) as [number, number, number] + const cameraPos = this.cameraObject.position.toArray().map(x => Math.floor(x / 16)) as [number, number, number] this.cameraSectionPos = new Vec3(...cameraPos) // eslint-disable-next-line guard-for-in for (const key in this.sectionObjects) { @@ -429,10 +431,8 @@ export class WorldRendererThree extends WorldRendererCommon { } setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number) { - const cam = this.cameraObjectOverride || this.camera const yOffset = this.displayOptions.playerState.getEyeHeight() - this.camera = cam as THREE.PerspectiveCamera this.updateCamera(pos?.offset(0, yOffset, 0) ?? null, yaw, pitch) this.media.tryIntersectMedia() } @@ -445,7 +445,11 @@ export class WorldRendererThree extends WorldRendererCommon { // } if (pos) { - new tweenJs.Tween(this.camera.position).to({ x: pos.x, y: pos.y, z: pos.z }, 50).start() + if (this.renderer.xr.isPresenting) { + pos.y -= this.camera.position.y // Fix Y position of camera in world + } + + new tweenJs.Tween(this.cameraObject.position).to({ x: pos.x, y: pos.y, z: pos.z }, 50).start() // this.freeFlyState.position = pos } this.cameraShake.setBaseRotation(pitch, yaw) @@ -467,13 +471,13 @@ export class WorldRendererThree extends WorldRendererCommon { this.entities.render() // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style - const cam = this.camera instanceof THREE.Group ? this.camera.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera + const cam = this.cameraGroupVr instanceof THREE.Group ? this.cameraGroupVr.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera this.renderer.render(this.scene, cam) - if (this.displayOptions.inWorldRenderingConfig.showHand && !this.playerState.shouldHideHand /* && !this.freeFlyMode */) { - this.holdingBlock.render(this.camera, this.renderer, this.ambientLight, this.directionalLight) - this.holdingBlockLeft.render(this.camera, this.renderer, this.ambientLight, this.directionalLight) - } + // if (this.displayOptions.inWorldRenderingConfig.showHand && !this.playerState.shouldHideHand /* && !this.freeFlyMode */) { + // this.holdingBlock.render(this.camera, this.renderer, this.ambientLight, this.directionalLight) + // this.holdingBlockLeft.render(this.camera, this.renderer, this.ambientLight, this.directionalLight) + // } for (const fountain of this.fountains) { if (this.sectionObjects[fountain.sectionId] && !this.sectionObjects[fountain.sectionId].foutain) { diff --git a/src/appViewer.ts b/src/appViewer.ts index 90ca847f..f4a21481 100644 --- a/src/appViewer.ts +++ b/src/appViewer.ts @@ -199,7 +199,7 @@ export class AppViewer { resetBackend (cleanState = false) { this.disconnectBackend(cleanState) if (this.backendLoader) { - this.loadBackend(this.backendLoader) + void this.loadBackend(this.backendLoader) } } diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx index ede367f5..7f4c34ab 100644 --- a/src/optionsGuiScheme.tsx +++ b/src/optionsGuiScheme.tsx @@ -488,7 +488,11 @@ export const guiOptionsScheme: { ) }, - vrSupport: {} + vrSupport: {}, + vrPageGameRendering: { + text: 'Page Game Rendering', + tooltip: 'Wether to continue rendering page even when vr is active.', + } }, ], advanced: [ diff --git a/src/optionsStorage.ts b/src/optionsStorage.ts index ab164454..a0c995a0 100644 --- a/src/optionsStorage.ts +++ b/src/optionsStorage.ts @@ -104,6 +104,7 @@ const defaultOptions = { autoJump: 'auto' as 'auto' | 'always' | 'never', autoParkour: false, vrSupport: true, // doesn't directly affect the VR mode, should only disable the button which is annoying to android users + vrPageGameRendering: false, renderDebug: 'basic' as 'none' | 'advanced' | 'basic', // advanced bot options diff --git a/src/watchOptions.ts b/src/watchOptions.ts index 9fe55289..903d7da8 100644 --- a/src/watchOptions.ts +++ b/src/watchOptions.ts @@ -80,6 +80,11 @@ export const watchOptionsAfterViewerInit = () => { updateFpsLimit(o) }) + watchValue(options, o => { + appViewer.inWorldRenderingConfig.vrSupport = o.vrSupport + appViewer.inWorldRenderingConfig.vrPageGameRendering = o.vrPageGameRendering + }) + watchValue(options, (o, isChanged) => { appViewer.inWorldRenderingConfig.clipWorldBelowY = o.clipWorldBelowY appViewer.inWorldRenderingConfig.extraBlockRenderers = !o.disableSignsMapsSupport From 99d05fc94b0608196e48738102de6114a02c6b54 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sun, 25 May 2025 16:16:14 +0300 Subject: [PATCH 005/181] improve stability of minimap (though full refactor is still needed) --- renderer/viewer/lib/mesher/mesher.ts | 8 +++++--- renderer/viewer/lib/mesher/shared.ts | 2 ++ renderer/viewer/lib/worldrendererCommon.ts | 4 +++- src/react/MinimapProvider.tsx | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/renderer/viewer/lib/mesher/mesher.ts b/renderer/viewer/lib/mesher/mesher.ts index 66621f22..bcfbde89 100644 --- a/renderer/viewer/lib/mesher/mesher.ts +++ b/renderer/viewer/lib/mesher/mesher.ts @@ -2,6 +2,7 @@ import { Vec3 } from 'vec3' import { World } from './world' import { getSectionGeometry, setBlockStatesData as setMesherData } from './models' import { BlockStateModelInfo } from './shared' +import { INVISIBLE_BLOCKS } from './worldConstants' globalThis.structuredClone ??= (value) => JSON.parse(JSON.stringify(value)) @@ -156,10 +157,11 @@ const handleMessage = data => { for (let x = 0; x < 16; x++) { const blockX = x + data.x const blockZ = z + data.z - blockPos.x = blockX; blockPos.z = blockZ - blockPos.y = 256 + blockPos.x = blockX + blockPos.z = blockZ + blockPos.y = world.config.worldMaxY let block = world.getBlock(blockPos) - while (block?.name.includes('air')) { + while (block && INVISIBLE_BLOCKS.has(block.name)) { blockPos.y -= 1 block = world.getBlock(blockPos) } diff --git a/renderer/viewer/lib/mesher/shared.ts b/renderer/viewer/lib/mesher/shared.ts index 16b89a71..82416fab 100644 --- a/renderer/viewer/lib/mesher/shared.ts +++ b/renderer/viewer/lib/mesher/shared.ts @@ -3,6 +3,8 @@ import { BlockType } from '../../../playground/shared' // only here for easier testing export const defaultMesherConfig = { version: '', + worldMaxY: 256, + worldMinY: 0, enableLighting: true, skyLight: 15, smoothLighting: true, diff --git a/renderer/viewer/lib/worldrendererCommon.ts b/renderer/viewer/lib/worldrendererCommon.ts index 26e3bf2e..3f517beb 100644 --- a/renderer/viewer/lib/worldrendererCommon.ts +++ b/renderer/viewer/lib/worldrendererCommon.ts @@ -560,7 +560,9 @@ export abstract class WorldRendererCommon textureSize: this.resourcesManager.currentResources!.blocksAtlasParser.atlas.latest.width, debugModelVariant: undefined, clipWorldBelowY: this.worldRendererConfig.clipWorldBelowY, - disableSignsMapsSupport: !this.worldRendererConfig.extraBlockRenderers + disableSignsMapsSupport: !this.worldRendererConfig.extraBlockRenderers, + worldMinY: this.worldMinYRender, + worldMaxY: this.worldMinYRender + this.worldSizeParams.worldHeight, } } diff --git a/src/react/MinimapProvider.tsx b/src/react/MinimapProvider.tsx index afb6a198..26a15134 100644 --- a/src/react/MinimapProvider.tsx +++ b/src/react/MinimapProvider.tsx @@ -45,7 +45,7 @@ export class DrawerAdapterImpl extends TypedEventEmitter implements chunksStore = new Map() loadingChunksQueue = new Set() loadChunk: (key: string) => Promise = this.loadChunkMinimap - mapDrawer = new MinimapDrawer(this.loadChunk, this.warps, this.loadingChunksQueue, this.chunksStore) + mapDrawer = new MinimapDrawer(this.loadChunk.bind(this), this.warps, this.loadingChunksQueue, this.chunksStore) currChunk: PCChunk | undefined currChunkPos: { x: number, z: number } = { x: 0, z: 0 } isOldVersion: boolean From 50907138f7803987bd148b3ba490e25d4fbf7b6a Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Mon, 26 May 2025 01:09:43 +0300 Subject: [PATCH 006/181] fix edge case infinite loop in mesher --- renderer/viewer/lib/mesher/mesher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renderer/viewer/lib/mesher/mesher.ts b/renderer/viewer/lib/mesher/mesher.ts index bcfbde89..f26d8022 100644 --- a/renderer/viewer/lib/mesher/mesher.ts +++ b/renderer/viewer/lib/mesher/mesher.ts @@ -161,7 +161,7 @@ const handleMessage = data => { blockPos.z = blockZ blockPos.y = world.config.worldMaxY let block = world.getBlock(blockPos) - while (block && INVISIBLE_BLOCKS.has(block.name)) { + while (block && INVISIBLE_BLOCKS.has(block.name) && blockPos.y > world.config.worldMinY) { blockPos.y -= 1 block = world.getBlock(blockPos) } From c500d08ed7e14e7ecbec76393a4a1c13e59d4dec Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Tue, 27 May 2025 11:30:21 +0300 Subject: [PATCH 007/181] hotfix: restore hand --- renderer/viewer/three/worldrendererThree.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index f8641ab8..d2d54998 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -474,10 +474,10 @@ export class WorldRendererThree extends WorldRendererCommon { const cam = this.cameraGroupVr instanceof THREE.Group ? this.cameraGroupVr.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera this.renderer.render(this.scene, cam) - // if (this.displayOptions.inWorldRenderingConfig.showHand && !this.playerState.shouldHideHand /* && !this.freeFlyMode */) { - // this.holdingBlock.render(this.camera, this.renderer, this.ambientLight, this.directionalLight) - // this.holdingBlockLeft.render(this.camera, this.renderer, this.ambientLight, this.directionalLight) - // } + if (this.displayOptions.inWorldRenderingConfig.showHand && !this.playerState.shouldHideHand /* && !this.freeFlyMode */ && !this.renderer.xr.isPresenting) { + this.holdingBlock.render(this.camera, this.renderer, this.ambientLight, this.directionalLight) + this.holdingBlockLeft.render(this.camera, this.renderer, this.ambientLight, this.directionalLight) + } for (const fountain of this.fountains) { if (this.sectionObjects[fountain.sectionId] && !this.sectionObjects[fountain.sectionId].foutain) { From 087e167826d6bf488410d3d46581103f330811fd Mon Sep 17 00:00:00 2001 From: Maksim Grigorev Date: Fri, 30 May 2025 16:43:06 +0300 Subject: [PATCH 008/181] feat: configurable mobile top buttons! (#361) --- config.json | 30 ++- src/appConfig.ts | 22 +++ src/controls.ts | 105 ++++++++++- src/globalState.ts | 1 + src/react/DebugOverlay.tsx | 8 +- src/react/MobileTopButtons.module.css | 14 ++ src/react/MobileTopButtons.module.css.d.ts | 2 + src/react/MobileTopButtons.tsx | 209 ++++++++++++++------- 8 files changed, 311 insertions(+), 80 deletions(-) diff --git a/config.json b/config.json index 29f011c0..3696af7a 100644 --- a/config.json +++ b/config.json @@ -37,5 +37,33 @@ "type": "discord" } ] + ], + "mobileButtons": [ + { + "action": "general.drop", + "actionHold": "", + "label": "Q" + }, + { + "action": "general.debugOverlay", + "actionHold": "general.debugOverlayHelpMenu", + "label": "F3" + }, + { + "action": "general.playersList", + "actionHold": "", + "icon": "pixelarticons:users", + "label": "TAB" + }, + { + "action": "general.chat", + "actionHold": "", + "label": "" + }, + { + "action": "ui.back", + "actionHold": "", + "label": "" + } ] -} \ No newline at end of file +} diff --git a/src/appConfig.ts b/src/appConfig.ts index 769ef14d..48b3665a 100644 --- a/src/appConfig.ts +++ b/src/appConfig.ts @@ -5,6 +5,27 @@ import { setLoadingScreenStatus } from './appStatus' import { setStorageDataOnAppConfigLoad } from './react/appStorageProvider' import { customKeymaps, updateBinds } from './controls' +export type CustomAction = { + readonly type: string + readonly input: readonly any[] +} + +export type ActionType = string | CustomAction + +export type ActionHoldConfig = { + readonly command: ActionType + readonly longPressAction?: ActionType + readonly duration?: number + readonly threshold?: number +} + +export type MobileButtonConfig = { + readonly label?: string + readonly icon?: string + readonly action?: ActionType + readonly actionHold?: ActionType | ActionHoldConfig +} + export type AppConfig = { // defaultHost?: string // defaultHostSave?: string @@ -26,6 +47,7 @@ export type AppConfig = { splashText?: string splashTextFallback?: string pauseLinks?: Array>> + mobileButtons?: MobileButtonConfig[] keybindings?: Record defaultLanguage?: string displayLanguageSelector?: boolean diff --git a/src/controls.ts b/src/controls.ts index d1e9b1e4..3a39798c 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -28,6 +28,7 @@ import { createNotificationProgressReporter } from './core/progressReporter' import { appStorage } from './react/appStorageProvider' import { switchGameMode } from './packetsReplay/replayPackets' import { tabListState } from './react/PlayerListOverlayProvider' +import { type ActionType, type ActionHoldConfig, type CustomAction } from './appConfig' export const customKeymaps = proxy(appStorage.keybindings) @@ -55,7 +56,7 @@ export const contro = new ControMax({ attackDestroy: [null, 'Right Trigger'], interactPlace: [null, 'Left Trigger'], swapHands: ['KeyF'], - selectItem: ['KeyH'], // default will be removed + selectItem: ['KeyH'], rotateCameraLeft: [null], rotateCameraRight: [null], rotateCameraUp: [null], @@ -63,10 +64,12 @@ export const contro = new ControMax({ // ui? chat: [['KeyT', 'Enter']], command: ['Slash'], + playersList: ['Tab'], + debugOverlay: ['F3'], + debugOverlayHelpMenu: [null], // client side zoom: ['KeyC'], viewerConsole: ['Backquote'], - playersList: ['Tab'], }, ui: { toggleFullscreen: ['F11'], @@ -234,7 +237,7 @@ const inModalCommand = (command: Command, pressed: boolean) => { if (pressed && !gamepadUiCursorState.display) return if (pressed) { - if (command === 'ui.back') { + if (command === 'ui.back' || command === 'ui.pauseMenu') { hideCurrentModal() } if (command === 'ui.leftClick' || command === 'ui.rightClick') { @@ -401,6 +404,27 @@ const onTriggerOrReleased = (command: Command, pressed: boolean) => { case 'general.zoom': gameAdditionalState.isZooming = pressed break + case 'general.debugOverlay': + if (pressed) { + hardcodedPressedKeys.add('F3') + const pressedKey = [...contro.pressedKeys].find(key => key !== 'F3') + if (pressedKey) { + const keybind = f3Keybinds.find((v) => v.key === pressedKey) + if (keybind && (keybind.enabled?.() ?? true)) { + void keybind.action() + } + } else { + miscUiState.showDebugHud = !miscUiState.showDebugHud + } + } else { + hardcodedPressedKeys.delete('F3') + } + break + case 'general.debugOverlayHelpMenu': + if (pressed) { + void onF3LongPress() + } + break case 'general.rotateCameraLeft': case 'general.rotateCameraRight': case 'general.rotateCameraUp': @@ -411,6 +435,26 @@ const onTriggerOrReleased = (command: Command, pressed: boolean) => { tabListState.isOpen = pressed break } + } else if (stringStartsWith(command, 'ui')) { + switch (command) { + case 'ui.back': + case 'ui.pauseMenu': + if (pressed) { + if (activeModalStack.length) { + hideCurrentModal() + } else { + showModal({ reactType: 'pause-screen' }) + } + } + break + case 'ui.toggleFullscreen': + case 'ui.toggleMap': + case 'ui.leftClick': + case 'ui.rightClick': + case 'ui.speedupCursor': + // These are handled elsewhere + break + } } } @@ -499,6 +543,8 @@ contro.on('trigger', ({ command }) => { case 'general.rotateCameraRight': case 'general.rotateCameraUp': case 'general.rotateCameraDown': + case 'general.debugOverlay': + case 'general.debugOverlayHelpMenu': case 'general.playersList': // no-op break @@ -567,10 +613,6 @@ contro.on('trigger', ({ command }) => { // toggleMicrophoneMuted() } - if (command === 'ui.pauseMenu') { - showModal({ reactType: 'pause-screen' }) - } - if (command === 'ui.toggleFullscreen') { void goFullscreen(true) } @@ -893,3 +935,52 @@ export function updateBinds (commands: any) { })) } } + +export const onF3LongPress = async () => { + const select = await showOptionsModal('', f3Keybinds.filter(f3Keybind => { + return f3Keybind.mobileTitle && (f3Keybind.enabled?.() ?? true) + }).map(f3Keybind => { + return `${f3Keybind.mobileTitle}${f3Keybind.key ? ` (F3+${f3Keybind.key})` : ''}` + })) + if (!select) return + const f3Keybind = f3Keybinds.find(f3Keybind => f3Keybind.mobileTitle === select) + if (f3Keybind) void f3Keybind.action() +} + +export const handleMobileButtonCustomAction = (action: CustomAction) => { + const handler = customCommandsConfig[action.type]?.handler + if (handler) { + handler([...action.input]) + } +} + +export const handleMobileButtonActionCommand = (command: ActionType | ActionHoldConfig, isDown: boolean) => { + const commandValue = typeof command === 'string' ? command : 'command' in command ? command.command : command + + if (typeof commandValue === 'string' && !stringStartsWith(commandValue, 'custom')) { + const event: CommandEventArgument = { + command: commandValue as Command, + schema: { + keys: [], + gamepad: [] + } + } + if (isDown) { + contro.emit('trigger', event) + } else { + contro.emit('release', event) + } + } else if (typeof commandValue === 'object') { + if (isDown) { + handleMobileButtonCustomAction(commandValue) + } + } +} + +export const handleMobileButtonLongPress = (actionHold: ActionHoldConfig) => { + if (typeof actionHold.longPressAction === 'string' && actionHold.longPressAction === 'general.debugOverlayHelpMenu') { + void onF3LongPress() + } else if (actionHold.longPressAction) { + handleMobileButtonActionCommand(actionHold.longPressAction, true) + } +} diff --git a/src/globalState.ts b/src/globalState.ts index bd845195..671d7907 100644 --- a/src/globalState.ts +++ b/src/globalState.ts @@ -121,6 +121,7 @@ export const miscUiState = proxy({ /** wether game hud is shown (in playing state) */ gameLoaded: false, showUI: true, + showDebugHud: false, loadedServerIndex: '', /** currently trying to load or loaded mc version, after all data is loaded */ loadedDataVersion: null as string | null, diff --git a/src/react/DebugOverlay.tsx b/src/react/DebugOverlay.tsx index 25c102b7..7b22c96a 100644 --- a/src/react/DebugOverlay.tsx +++ b/src/react/DebugOverlay.tsx @@ -1,6 +1,8 @@ import { useEffect, useRef, useState } from 'react' +import { useSnapshot } from 'valtio' import type { Block } from 'prismarine-block' import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods' +import { miscUiState } from '../globalState' import { getFixedFilesize } from '../downloadAndOpenFile' import { options } from '../optionsStorage' import { BlockStateModelInfo } from '../../renderer/viewer/lib/mesher/shared' @@ -28,7 +30,7 @@ export default () => { window.packetsCountByName = packetsCountByName const ignoredPackets = useRef(new Set([] as any[])) const [packetsString, setPacketsString] = useState('') - const [showDebug, setShowDebug] = useState(false) + const { showDebugHud } = useSnapshot(miscUiState) const [pos, setPos] = useState<{ x: number, y: number, z: number }>({ x: 0, y: 0, z: 0 }) const [skyL, setSkyL] = useState(0) const [blockL, setBlockL] = useState(0) @@ -55,7 +57,7 @@ export default () => { const handleF3 = (e) => { if (e.code === 'F3') { - setShowDebug(prev => !prev) + miscUiState.showDebugHud = !miscUiState.showDebugHud e.preventDefault() } } @@ -174,7 +176,7 @@ export default () => { minecraftQuad.current = Math.floor(((minecraftYaw.current + 180) / 90 + 0.5) % 4) }, [bot.entity.yaw]) - if (!showDebug) return null + if (!showDebugHud) return null return <>
diff --git a/src/react/MobileTopButtons.module.css b/src/react/MobileTopButtons.module.css index d1692f8b..55246492 100644 --- a/src/react/MobileTopButtons.module.css +++ b/src/react/MobileTopButtons.module.css @@ -35,3 +35,17 @@ padding: 4px 6px; outline: 0.5px solid white; } + +.tab-btn { + color: #fff; + background: #9c8c86; + outline: .5px solid #fff; + padding: 4px 6px; + font-family: pixelarticons, mojangles, monospace; + font-size: 8px; +} + +.tab-btn:has(> div) { + padding: 3px 5px; + font-size: 12px; +} diff --git a/src/react/MobileTopButtons.module.css.d.ts b/src/react/MobileTopButtons.module.css.d.ts index 859cc929..6b38dde0 100644 --- a/src/react/MobileTopButtons.module.css.d.ts +++ b/src/react/MobileTopButtons.module.css.d.ts @@ -9,6 +9,8 @@ interface CssExports { mobileTopBtns: string; 'pause-btn': string; pauseBtn: string; + 'tab-btn': string; + tabBtn: string; } declare const cssExports: CssExports; export default cssExports; diff --git a/src/react/MobileTopButtons.tsx b/src/react/MobileTopButtons.tsx index a2f1a1da..e8a82f77 100644 --- a/src/react/MobileTopButtons.tsx +++ b/src/react/MobileTopButtons.tsx @@ -1,91 +1,162 @@ import { useEffect, useRef } from 'react' -import { f3Keybinds } from '../controls' +import { useSnapshot } from 'valtio' +import { handleMobileButtonActionCommand, handleMobileButtonLongPress } from '../controls' import { watchValue } from '../optionsStorage' -import { showModal, miscUiState, activeModalStack, hideCurrentModal } from '../globalState' -import { showOptionsModal } from './SelectOption' -import useLongPress from './useLongPress' +import { type MobileButtonConfig, type ActionHoldConfig, type ActionType, type CustomAction } from '../appConfig' +import { miscUiState } from '../globalState' +import PixelartIcon from './PixelartIcon' import styles from './MobileTopButtons.module.css' - export default () => { const elRef = useRef(null) + const { appConfig } = useSnapshot(miscUiState) + const mobileButtonsConfig = appConfig?.mobileButtons - const showMobileControls = (bl) => { - if (elRef.current) elRef.current.style.display = bl ? 'flex' : 'none' + const longPressTimerIdRef = useRef(null) + const actionToShortPressRef = useRef(null) + + const showMobileControls = (visible: boolean) => { + if (elRef.current) { + elRef.current.style.display = visible ? 'flex' : 'none' + } } useEffect(() => { watchValue(miscUiState, o => { - showMobileControls(o.currentTouch) + showMobileControls(Boolean(o.currentTouch)) }) }, []) - const onLongPress = async () => { - const select = await showOptionsModal('', f3Keybinds.filter(f3Keybind => { - return f3Keybind.mobileTitle && (f3Keybind.enabled?.() ?? true) - }).map(f3Keybind => { - return `${f3Keybind.mobileTitle}${f3Keybind.key ? ` (F3+${f3Keybind.key})` : ''}` - })) - if (!select) return - const f3Keybind = f3Keybinds.find(f3Keybind => f3Keybind.mobileTitle === select) - if (f3Keybind) void f3Keybind.action() - } + const getButtonClassName = (button: MobileButtonConfig): string => { + const actionForStyle = button.action || (button.actionHold && typeof button.actionHold === 'object' && 'command' in button.actionHold ? button.actionHold.command : undefined) - const defaultOptions = { - shouldPreventDefault: true, - delay: 500, - } - const longPressEvent = useLongPress(onLongPress, () => {}, defaultOptions) - - - const onChatLongPress = () => { - document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' })) - } - - const onChatClick = () => { - if (activeModalStack.at(-1)?.reactType === 'chat') { - hideCurrentModal() - } else { - showModal({ reactType: 'chat' }) + if (typeof actionForStyle === 'string') { + switch (actionForStyle) { + case 'general.chat': + return styles['chat-btn'] + case 'ui.back': + return styles['pause-btn'] + case 'general.playersList': + return styles['tab-btn'] + default: + return styles['debug-btn'] + } } + return styles['debug-btn'] } - const chatLongPressEvent = useLongPress( - onChatLongPress, - onChatClick, - { - shouldPreventDefault: true, - delay: 300, - } - ) + const renderConfigButtons = () => { + return mobileButtonsConfig?.map((button, index) => { + const className = getButtonClassName(button) + let label: string | JSX.Element = button.icon || button.label || '' + + if (typeof label === 'string' && label.startsWith('pixelarticons:')) { + const iconName = label.replace('pixelarticons:', '') + label = + } + + const onPointerDown = (e: React.PointerEvent) => { + const elem = e.currentTarget as HTMLElement + elem.setPointerCapture(e.pointerId) + + if (longPressTimerIdRef.current) { + clearTimeout(longPressTimerIdRef.current) + longPressTimerIdRef.current = null + } + actionToShortPressRef.current = null + + const { actionHold, action } = button + + if (actionHold) { + if (typeof actionHold === 'object' && 'command' in actionHold) { + const config = actionHold + if (config.longPressAction) { + actionToShortPressRef.current = config.command + longPressTimerIdRef.current = window.setTimeout(() => { + handleMobileButtonLongPress(config) + actionToShortPressRef.current = null + longPressTimerIdRef.current = null + }, config.duration || 500) + } else { + handleMobileButtonActionCommand(config.command, true) + } + } else if (action) { + actionToShortPressRef.current = action + longPressTimerIdRef.current = window.setTimeout(() => { + handleMobileButtonActionCommand(actionHold, true) + actionToShortPressRef.current = null + longPressTimerIdRef.current = null + }, 500) + } else { + handleMobileButtonActionCommand(actionHold, true) + } + } else if (action) { + handleMobileButtonActionCommand(action, true) + } + } + + const onPointerUp = (e: React.PointerEvent) => { + const elem = e.currentTarget as HTMLElement + elem.releasePointerCapture(e.pointerId) + + const { actionHold, action } = button + let wasShortPressHandled = false + + if (longPressTimerIdRef.current) { + clearTimeout(longPressTimerIdRef.current) + longPressTimerIdRef.current = null + if (actionToShortPressRef.current) { + handleMobileButtonActionCommand(actionToShortPressRef.current, true) + handleMobileButtonActionCommand(actionToShortPressRef.current, false) + wasShortPressHandled = true + } + } + + if (!wasShortPressHandled) { + if (actionHold) { + if (typeof actionHold === 'object' && 'command' in actionHold) { + const config = actionHold + if (config.longPressAction) { + if (actionToShortPressRef.current === null) { + if (typeof config.longPressAction === 'string') { + handleMobileButtonActionCommand(config.longPressAction, false) + } + } + } else { + handleMobileButtonActionCommand(config.command, false) + } + } else if (action) { + if (actionToShortPressRef.current === null) { + handleMobileButtonActionCommand(actionHold, false) + } + } else { + handleMobileButtonActionCommand(actionHold, false) + } + } else if (action) { + handleMobileButtonActionCommand(action, false) + } + } + actionToShortPressRef.current = null + } + + return ( +
+ {label} +
+ ) + }) + } // ios note: just don't use
+ +
+

Chunks Render Settings

+
+
+ +
+
+ + {/*
+
*/} +
+ +} diff --git a/src/react/button.module.css b/src/react/button.module.css index 677a1b44..e940c224 100644 --- a/src/react/button.module.css +++ b/src/react/button.module.css @@ -4,6 +4,7 @@ position: relative; width: 200px; min-height: calc(20px * var(--scale)); + max-height: calc(20px * var(--scale)); font-family: minecraft, mojangles, monospace; font-size: 10px; color: white; diff --git a/src/react/rendererDebugMenu.module.css b/src/react/rendererDebugMenu.module.css new file mode 100644 index 00000000..6c49e04c --- /dev/null +++ b/src/react/rendererDebugMenu.module.css @@ -0,0 +1,34 @@ +.container { + display: flex; + flex-direction: row; + justify-content: space-between; + gap: 20px; + padding: 10px; + height: 100%; + padding-top: env(safe-area-inset-top, 10px); + padding-left: env(safe-area-inset-left, 10px); + padding-right: env(safe-area-inset-right, 10px); + padding-bottom: env(safe-area-inset-bottom, 10px); +} + +.column { + display: flex; + flex-direction: column; + gap: 10px; + min-width: 200px; +} + +.column h3 { + margin: 0; + padding: 0; + font-size: 16px; + color: white; + text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.8); +} + +.sliderGroup { + display: flex; + flex-direction: column; + gap: 5px; + margin-bottom: 10px; +} diff --git a/src/react/rendererDebugMenu.module.css.d.ts b/src/react/rendererDebugMenu.module.css.d.ts new file mode 100644 index 00000000..0f8f2163 --- /dev/null +++ b/src/react/rendererDebugMenu.module.css.d.ts @@ -0,0 +1,9 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + column: string; + container: string; + sliderGroup: string; +} +declare const cssExports: CssExports; +export default cssExports; diff --git a/src/reactUi.tsx b/src/reactUi.tsx index 6405b27e..e67facea 100644 --- a/src/reactUi.tsx +++ b/src/reactUi.tsx @@ -61,6 +61,7 @@ import ControDebug from './react/ControDebug' import ChunksDebug from './react/ChunksDebug' import ChunksDebugScreen from './react/ChunksDebugScreen' import DebugResponseTimeIndicator from './react/debugs/DebugResponseTimeIndicator' +import RendererDebugMenu from './react/RendererDebugMenu' import CreditsAboutModal from './react/CreditsAboutModal' import GlobalOverlayHints from './react/GlobalOverlayHints' @@ -167,6 +168,7 @@ const InGameUi = () => { {!disabledUiParts.includes('bossbars') && displayBossBars && } + diff --git a/src/watchOptions.ts b/src/watchOptions.ts index 903d7da8..478da4fb 100644 --- a/src/watchOptions.ts +++ b/src/watchOptions.ts @@ -83,6 +83,7 @@ export const watchOptionsAfterViewerInit = () => { watchValue(options, o => { appViewer.inWorldRenderingConfig.vrSupport = o.vrSupport appViewer.inWorldRenderingConfig.vrPageGameRendering = o.vrPageGameRendering + appViewer.inWorldRenderingConfig.enableDebugOverlay = o.rendererPerfDebugOverlay }) watchValue(options, (o, isChanged) => { From 1c93fd7f602c46ebe9554b5c33ed7d60ab5cca7b Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Tue, 10 Jun 2025 05:22:52 +0300 Subject: [PATCH 015/181] fix wording --- src/optionsGuiScheme.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx index c74a72d8..422e1768 100644 --- a/src/optionsGuiScheme.tsx +++ b/src/optionsGuiScheme.tsx @@ -362,7 +362,7 @@ export const guiOptionsScheme: { }, { custom () { - return + return }, }, { From e982bf1493207575e2e077a4843188083c10b794 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Tue, 10 Jun 2025 05:24:28 +0300 Subject: [PATCH 016/181] fix(important): make chat word breaking match Minecraftt behavior --- src/react/Chat.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/Chat.css b/src/react/Chat.css index e6a62ab2..f1e92338 100644 --- a/src/react/Chat.css +++ b/src/react/Chat.css @@ -188,7 +188,7 @@ input[type=text], padding-left: 4px; background-color: rgba(0, 0, 0, 0.5); list-style: none; - word-break: break-all; + overflow-wrap: break-word; } .chat-message-fadeout { From 55759335590c580edc1189cd83a1ad10d5d454ce Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Tue, 10 Jun 2025 05:26:32 +0300 Subject: [PATCH 017/181] change title for mcraft.fun version --- .github/workflows/release.yml | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 422b6e64..14c4b471 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,6 +40,20 @@ jobs: mkdir -p .vercel/output/static/playground pnpm build-playground cp -r renderer/dist/* .vercel/output/static/playground/ + + # publish to github + - run: cp vercel.json .vercel/output/static/vercel.json + - uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: .vercel/output/static + force_orphan: true + + - name: Change index.html title + run: | + # change Minecraft Web Client to Minecraft Web Client — Free Online Browser Version + sed -i 's/Minecraft Web Client<\/title>/<title>Minecraft Web Client — Free Online Browser Version<\/title>/' .vercel/output/static/index.html + - name: Deploy Project to Vercel uses: mathiasvr/command-output@v2.0.0 with: @@ -53,13 +67,6 @@ jobs: for alias in $(echo ${{ steps.alias.outputs.alias }} | tr "," "\n"); do vercel alias set ${{ steps.deploy.outputs.stdout }} $alias --token=${{ secrets.VERCEL_TOKEN }} --scope=zaro done - # publish to github - - run: cp vercel.json .vercel/output/static/vercel.json - - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: .vercel/output/static - force_orphan: true - name: Build single-file version - minecraft.html run: pnpm build-single-file && mv dist/single/index.html minecraft.html From 6a583d2a363f04793265cc5afa1a766f2fd20724 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Tue, 10 Jun 2025 06:33:01 +0300 Subject: [PATCH 018/181] feat: Custom chat ping functionality!!! To ping on any server type @ and on other web client such ping will be highlighted. Can be disabled. todo: enable a way to display ping-only messages todo: reply (public/private) command todo: fix copmletion offset --- src/optionsGuiScheme.tsx | 3 ++ src/optionsStorage.ts | 1 + src/react/Chat.tsx | 83 ++++++++++++++++++++++++++++++++------ src/react/ChatProvider.tsx | 7 +++- 4 files changed, 81 insertions(+), 13 deletions(-) diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx index 422e1768..e3b71778 100644 --- a/src/optionsGuiScheme.tsx +++ b/src/optionsGuiScheme.tsx @@ -286,7 +286,10 @@ export const guiOptionsScheme: { chatOpacityOpened: { }, chatSelect: { + text: 'Text Select', }, + chatPingExtension: { + } }, { custom () { diff --git a/src/optionsStorage.ts b/src/optionsStorage.ts index 88e86817..59f222bd 100644 --- a/src/optionsStorage.ts +++ b/src/optionsStorage.ts @@ -75,6 +75,7 @@ const defaultOptions = { debugChatScroll: false, chatVanillaRestrictions: true, debugResponseTimeIndicator: false, + chatPingExtension: true, // antiAliasing: false, clipWorldBelowY: undefined as undefined | number, // will be removed diff --git a/src/react/Chat.tsx b/src/react/Chat.tsx index c2e59b07..8b470910 100644 --- a/src/react/Chat.tsx +++ b/src/react/Chat.tsx @@ -15,7 +15,7 @@ export type Message = { faded?: boolean } -const MessageLine = ({ message }: { message: Message }) => { +const MessageLine = ({ message, currentPlayerName }: { message: Message, currentPlayerName?: string }) => { const classes = { 'chat-message-fadeout': message.fading, 'chat-message-fade': message.fading, @@ -24,7 +24,27 @@ const MessageLine = ({ message }: { message: Message }) => { } return <li className={Object.entries(classes).filter(([, val]) => val).map(([name]) => name).join(' ')}> - {message.parts.map((msg, i) => <MessagePart key={i} part={msg} />)} + {message.parts.map((msg, i) => { + // Check if this is a text part that might contain a mention + if (msg.text && currentPlayerName) { + const parts = msg.text.split(new RegExp(`(@${currentPlayerName})`, 'i')) + if (parts.length > 1) { + return parts.map((txtPart, j) => { + const part = { + ...msg, + text: txtPart + } + if (txtPart.toLowerCase() === `@${currentPlayerName}`.toLowerCase()) { + part.color = '#ffa500' + part.bold = true + return <MessagePart key={j} part={part} /> + } + return <MessagePart key={j} part={part} /> + }) + } + } + return <MessagePart key={i} part={msg} /> + })} </li> } @@ -42,6 +62,8 @@ type Props = { placeholder?: string chatVanillaRestrictions?: boolean debugChatScroll?: boolean + getPingComplete?: (value: string) => Promise<string[]> + currentPlayerName?: string } export const chatInputValueGlobal = proxy({ @@ -71,12 +93,19 @@ export default ({ inputDisabled, placeholder, chatVanillaRestrictions, - debugChatScroll + debugChatScroll, + getPingComplete, + currentPlayerName }: Props) => { + const playerNameValidated = useMemo(() => { + if (!/^[\w\d_]+$/i.test(currentPlayerName ?? '')) return '' + return currentPlayerName + }, [currentPlayerName]) + const sendHistoryRef = useRef(JSON.parse(window.sessionStorage.chatHistory || '[]')) const [isInputFocused, setIsInputFocused] = useState(false) - // const [spellCheckEnabled, setSpellCheckEnabled] = useState(false) const spellCheckEnabled = false + const pingHistoryRef = useRef(JSON.parse(window.localStorage.pingHistory || '[]')) const [completePadText, setCompletePadText] = useState('') const completeRequestValue = useRef('') @@ -108,9 +137,13 @@ export default ({ const acceptComplete = (item: string) => { const base = completeRequestValue.current === '/' ? '' : getCompleteValue() updateInputValue(base + item) - // todo would be cool but disabled because some comands don't need args (like ping) - // // trigger next tab complete - // this.chatInput.dispatchEvent(new KeyboardEvent('keydown', { code: 'Space' })) + // Record ping completion in history + if (item.startsWith('@')) { + const newHistory = [item, ...pingHistoryRef.current.filter((x: string) => x !== item)].slice(0, 10) + pingHistoryRef.current = newHistory + // todo use appStorage + window.localStorage.pingHistory = JSON.stringify(newHistory) + } chatInput.current.focus() } @@ -200,6 +233,13 @@ export default ({ }, [opened]) const onMainInputChange = () => { + const lastWord = chatInput.current.value.slice(0, chatInput.current.selectionEnd ?? chatInput.current.value.length).split(' ').at(-1)! + if (lastWord.startsWith('@') && getPingComplete) { + setCompletePadText(lastWord) + void fetchPingCompletions(true, lastWord.slice(1)) + return + } + const completeValue = getCompleteValue() setCompletePadText(completeValue === '/' ? '' : completeValue) // not sure if enabling would be useful at all (maybe make as a setting in the future?) @@ -215,9 +255,6 @@ export default ({ resetCompletionItems() } completeRequestValue.current = completeValue - // if (completeValue === '/') { - // void fetchCompletions(true) - // } } const fetchCompletions = async (implicit: boolean, inputValue = chatInput.current.value) => { @@ -230,6 +267,24 @@ export default ({ updateFilteredCompleteItems(newItems) } + const fetchPingCompletions = async (implicit: boolean, inputValue: string) => { + completeRequestValue.current = inputValue + resetCompletionItems() + const newItems = await getPingComplete?.(inputValue) ?? [] + if (inputValue !== completeRequestValue.current) return + // Sort items by ping history + const sortedItems = [...newItems].sort((a, b) => { + const aIndex = pingHistoryRef.current.indexOf(a) + const bIndex = pingHistoryRef.current.indexOf(b) + if (aIndex === -1 && bIndex === -1) return 0 + if (aIndex === -1) return 1 + if (bIndex === -1) return -1 + return aIndex - bIndex + }) + setCompletionItemsSource(sortedItems) + updateFilteredCompleteItems(sortedItems) + } + const updateFilteredCompleteItems = (sourceItems: string[] | Array<{ match: string, toolip: string }>) => { const newCompleteItems = sourceItems .map((item): string => (typeof item === 'string' ? item : item.match)) @@ -237,8 +292,11 @@ export default ({ // this regex is imporatnt is it controls the word matching // const compareableParts = item.split(/[[\]{},_:]/) const lastWord = chatInput.current.value.slice(0, chatInput.current.selectionEnd ?? chatInput.current.value.length).split(' ').at(-1)! - // return [item, ...compareableParts].some(compareablePart => compareablePart.startsWith(lastWord)) + if (lastWord.startsWith('@')) { + return item.toLowerCase().includes(lastWord.slice(1).toLowerCase()) + } return item.includes(lastWord) + // return [item, ...compareableParts].some(compareablePart => compareablePart.startsWith(lastWord)) }) setCompletionItems(newCompleteItems) } @@ -247,6 +305,7 @@ export default ({ const raw = chatInput.current.value return raw.slice(0, chatInput.current.selectionEnd ?? raw.length) } + const getCompleteValue = (value = getDefaultCompleteValue()) => { const valueParts = value.split(' ') const lastLength = valueParts.at(-1)!.length @@ -313,7 +372,7 @@ export default ({ </div> )} {messages.map((m) => ( - <MessageLine key={reactKeyForMessage(m)} message={m} /> + <MessageLine key={reactKeyForMessage(m)} message={m} currentPlayerName={playerNameValidated} /> ))} </div> || undefined} </div> diff --git a/src/react/ChatProvider.tsx b/src/react/ChatProvider.tsx index 5c499029..83691b83 100644 --- a/src/react/ChatProvider.tsx +++ b/src/react/ChatProvider.tsx @@ -17,7 +17,7 @@ export default () => { const isChatActive = useIsModalActive('chat') const lastMessageId = useRef(0) const usingTouch = useSnapshot(miscUiState).currentTouch - const { chatSelect, messagesLimit, chatOpacity, chatOpacityOpened, chatVanillaRestrictions, debugChatScroll } = useSnapshot(options) + const { chatSelect, messagesLimit, chatOpacity, chatOpacityOpened, chatVanillaRestrictions, debugChatScroll, chatPingExtension } = useSnapshot(options) const isUsingMicrosoftAuth = useMemo(() => !!lastConnectOptions.value?.authenticatedAccount, []) const { forwardChat } = useSnapshot(viewerVersionState) const { viewerConnection } = useSnapshot(gameAdditionalState) @@ -55,6 +55,11 @@ export default () => { messages={messages} opened={isChatActive} placeholder={forwardChat || !viewerConnection ? undefined : 'Chat forwarding is not enabled in the plugin settings'} + currentPlayerName={chatPingExtension ? bot.username : undefined} + getPingComplete={async (value) => { + const players = Object.keys(bot.players) + return players.filter(name => (!value || name.toLowerCase().includes(value.toLowerCase())) && name !== bot.username).map(name => `@${name}`) + }} sendMessage={async (message) => { const builtinHandled = tryHandleBuiltinCommand(message) if (getServerIndex() !== undefined && (message.startsWith('/login') || message.startsWith('/register'))) { From a562316cba91ad620484611836b6783d1d6bf4ac Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Tue, 10 Jun 2025 06:33:18 +0300 Subject: [PATCH 019/181] rm dead servers --- config.json | 7 ------- 1 file changed, 7 deletions(-) diff --git a/config.json b/config.json index bdfd28b2..35c22a1a 100644 --- a/config.json +++ b/config.json @@ -9,16 +9,9 @@ { "ip": "wss://play.mcraft.fun" }, - { - "ip": "wss://ws.fuchsmc.net" - }, { "ip": "wss://play2.mcraft.fun" }, - { - "ip": "wss://mcraft.ryzyn.xyz", - "version": "1.19.4" - }, { "ip": "kaboom.pw", "version": "1.20.3", From 14effc7400067ccb7dde32581f74bbbf9f40b7fc Mon Sep 17 00:00:00 2001 From: Vitaly <vital2580@icloud.com> Date: Tue, 10 Jun 2025 07:26:50 +0300 Subject: [PATCH 020/181] Should fix lint --- src/react/Chat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/Chat.tsx b/src/react/Chat.tsx index 8b470910..41a6cb6f 100644 --- a/src/react/Chat.tsx +++ b/src/react/Chat.tsx @@ -101,7 +101,7 @@ export default ({ if (!/^[\w\d_]+$/i.test(currentPlayerName ?? '')) return '' return currentPlayerName }, [currentPlayerName]) - + const sendHistoryRef = useRef(JSON.parse(window.sessionStorage.chatHistory || '[]')) const [isInputFocused, setIsInputFocused] = useState(false) const spellCheckEnabled = false From cfce8989184a60c2385b31f6a21c4d7e1562ce44 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Wed, 11 Jun 2025 03:19:14 +0300 Subject: [PATCH 021/181] make random username configurable --- config.json | 1 + src/appConfig.ts | 3 ++- src/appStatus.ts | 1 + src/react/appStorageProvider.ts | 9 +++++++-- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/config.json b/config.json index 35c22a1a..8fd264fb 100644 --- a/config.json +++ b/config.json @@ -31,6 +31,7 @@ } ] ], + "defaultUsername": "mcrafter{num}{num}{num}{num}", "mobileButtons": [ { "action": "general.drop", diff --git a/src/appConfig.ts b/src/appConfig.ts index 48b3665a..261ec7d3 100644 --- a/src/appConfig.ts +++ b/src/appConfig.ts @@ -53,6 +53,7 @@ export type AppConfig = { displayLanguageSelector?: boolean supportedLanguages?: string[] showModsButton?: boolean + defaultUsername?: string } export const loadAppConfig = (appConfig: AppConfig) => { @@ -81,7 +82,7 @@ export const loadAppConfig = (appConfig: AppConfig) => { updateBinds(customKeymaps) } - setStorageDataOnAppConfigLoad() + setStorageDataOnAppConfigLoad(appConfig) } export const isBundledConfigUsed = !!process.env.INLINED_APP_CONFIG diff --git a/src/appStatus.ts b/src/appStatus.ts index d3bfc461..054975f3 100644 --- a/src/appStatus.ts +++ b/src/appStatus.ts @@ -3,6 +3,7 @@ import { appStatusState, resetAppStatusState } from './react/AppStatusProvider' let ourLastStatus: string | undefined = '' export const setLoadingScreenStatus = function (status: string | undefined | null, isError = false, hideDots = false, fromFlyingSquid = false, minecraftJsonMessage?: Record<string, any>) { + if (typeof status === 'string') status = window.translateText?.(status) ?? status // null can come from flying squid, should restore our last status if (status === null) { status = ourLastStatus diff --git a/src/react/appStorageProvider.ts b/src/react/appStorageProvider.ts index 5cef34c1..32617426 100644 --- a/src/react/appStorageProvider.ts +++ b/src/react/appStorageProvider.ts @@ -1,6 +1,7 @@ import { proxy, ref, subscribe } from 'valtio' import { UserOverridesConfig } from 'contro-max/build/types/store' import { subscribeKey } from 'valtio/utils' +import { AppConfig } from '../appConfig' import { CustomCommand } from './KeybindingsCustom' import { AuthenticatedAccount } from './serversStorage' import type { BaseServerInfo } from './AddServerOrConnect' @@ -86,8 +87,12 @@ const defaultStorageData: StorageData = { firstModsPageVisit: true, } -export const setStorageDataOnAppConfigLoad = () => { - appStorage.username ??= `mcrafter${Math.floor(Math.random() * 1000)}` +export const setStorageDataOnAppConfigLoad = (appConfig: AppConfig) => { + appStorage.username ??= getRandomUsername(appConfig) +} + +export const getRandomUsername = (appConfig: AppConfig) => { + return appConfig.defaultUsername?.replaceAll('{num}', () => Math.floor(Math.random() * 10).toString()) ?? '' } export const appStorage = proxy({ ...defaultStorageData }) From ac7d28760f5d12cf7d8337c7132b9b84345cc2c8 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Fri, 13 Jun 2025 04:51:48 +0300 Subject: [PATCH 022/181] feat: Implement always up-to-date recommended servers display! Fix other annoying issues in servers list --- src/react/Button.tsx | 60 +++++++++++++++++++++++---- src/react/ServersListProvider.tsx | 67 +++++++++++++++++++++---------- src/react/Singleplayer.tsx | 44 +++++++++++--------- src/react/appStorageProvider.ts | 1 + src/react/serversStorage.ts | 20 --------- 5 files changed, 123 insertions(+), 69 deletions(-) diff --git a/src/react/Button.tsx b/src/react/Button.tsx index 1ef9be33..bad726f0 100644 --- a/src/react/Button.tsx +++ b/src/react/Button.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames' -import { createContext, FC, Ref, useContext } from 'react' +import { createContext, FC, Ref, useContext, useEffect, useRef } from 'react' import buttonCss from './button.module.css' import SharedHudVars from './SharedHudVars' import PixelartIcon from './PixelartIcon' @@ -26,19 +26,48 @@ export const ButtonProvider: FC<{ children, onClick }> = ({ children, onClick }) } export default (({ label, icon, children, inScreen, rootRef, type = 'button', postLabel, overlayColor, noTranslate, ...args }) => { + const style = { + ...args.style, + } as React.CSSProperties + + const buttonRef = useRef<any>(null) + + useEffect(() => { + // replace all text childs with translated text + // traverse dom + const traverse = (node: HTMLElement) => { + for (const child of node.childNodes) { + if (child.nodeType === Node.TEXT_NODE) { + if (child.textContent) { + child.textContent = translate(child.textContent) + } + } else { + traverse(child as HTMLElement) + } + } + } + traverse(buttonRef.current) + }, [label, children, postLabel]) + + args.title = translate(args.title) + const ctx = useContext(ButtonContext) const onClick = (e) => { ctx.onClick() args.onClick?.(e) } + const labelText = `${translate(label) ?? ''} ${typeof children === 'string' ? translate(children) : ''}` if (inScreen) { - args.style ??= {} - args.style.width = 150 + style.width = 150 + args.className = `${args.className ?? ''} settings-text-container` } + if (typeof style.width === 'number' && ((style.width <= 150 && labelText.length > 17) || (style.width >= 90 && labelText.length >= 11))) { + args.className = `${args.className ?? ''} settings-text-container-long` + } + if (icon) { - args.style ??= {} - args.style.width = 20 + style.width = 20 } const tryToTranslate = (maybeText: any) => { @@ -53,11 +82,26 @@ export default (({ label, icon, children, inScreen, rootRef, type = 'button', po } return <SharedHudVars> - <button ref={rootRef} {...args} className={classNames(buttonCss.button, args.className)} onClick={onClick} type={type}> + <button + ref={(button) => { + buttonRef.current = button + if (typeof rootRef === 'function') { + rootRef(button) + } else if (rootRef) { + //@ts-expect-error + rootRef.current = button + } + }} + {...args} + style={style} + className={classNames(buttonCss.button, args.className)} + onClick={onClick} + type={type} + > {icon && <PixelartIcon className={buttonCss.icon} iconName={icon} />} - {tryToTranslate(label)} + {label} {postLabel} - {tryToTranslate(children)} + {children} {overlayColor && <div style={{ position: 'absolute', inset: 0, diff --git a/src/react/ServersListProvider.tsx b/src/react/ServersListProvider.tsx index 8692e0bd..d95d4e9e 100644 --- a/src/react/ServersListProvider.tsx +++ b/src/react/ServersListProvider.tsx @@ -14,7 +14,7 @@ import { useDidUpdateEffect } from './utils' import { useIsModalActive } from './utilsApp' import { showOptionsModal } from './SelectOption' import { useCopyKeybinding } from './simpleHooks' -import { AuthenticatedAccount, getInitialServersList, getServerConnectionHistory, setNewServersList } from './serversStorage' +import { AuthenticatedAccount, setNewServersList } from './serversStorage' import { appStorage, StoreServerItem } from './appStorageProvider' import Button from './Button' import { pixelartIcons } from './PixelartIcon' @@ -45,10 +45,17 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL const { authenticatedAccounts } = useSnapshot(appStorage) const [quickConnectIp, setQuickConnectIp] = useState('') const [selectedIndex, setSelectedIndex] = useState(0) + const [retriggerFocusCounter, setRetriggerFocusCounter] = useState(0) - const { serversList: savedServersList } = useSnapshot(appStorage) + useEffect(() => { + if (!hidden) { + setRetriggerFocusCounter(x => x + 1) + } + }, [hidden]) - const serversListDisplay = useMemo(() => { + const _savedServersListWatchOnly = useSnapshot(appStorage).serversList + + const serversListProvided = useMemo(() => { return ( customServersList ? customServersList.map((row): StoreServerItem => { @@ -60,9 +67,9 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL name, } }) - : [...getInitialServersList()] + : [...(appStorage.serversList?.filter(server => server) ?? [])] ) - }, [customServersList, savedServersList]) + }, [customServersList, _savedServersListWatchOnly]) const [additionalServerData, setAdditionalServerData] = useState<Record<string, AdditionalDisplayData>>({}) @@ -78,14 +85,20 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL const newIndex = e.key === 'ArrowUp' ? Math.max(0, selectedIndex - 1) - : Math.min(serversListDisplay.length - 1, selectedIndex + 1) + : Math.min(serversListProvided.length - 1, selectedIndex + 1) if (newIndex === selectedIndex) return + if (newIndex < 0 || newIndex >= serversListProvided.length) return // Move server in the list - const newList = [...serversListDisplay] + const newList = [...serversListProvided] const oldItem = newList[selectedIndex] - newList[selectedIndex] = newList[newIndex] + const newItem = newList[newIndex] + if (oldItem.isRecommended || newItem.isRecommended) { + return + } + + newList[selectedIndex] = newItem newList[newIndex] = oldItem appStorage.serversList = newList @@ -94,9 +107,17 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL document.addEventListener('keydown', handleKeyDown) return () => document.removeEventListener('keydown', handleKeyDown) - }, [selectedIndex, serversListDisplay]) + }, [selectedIndex, serversListProvided]) - const serversListSorted = useMemo(() => serversListDisplay.map((server, index) => ({ ...server, index })), [serversListDisplay]) + const serversListSorted: Array<StoreServerItem & { index: number }> = useMemo(() => [ + ...serversListProvided, + ...(customServersList ? [] : (miscUiState.appConfig?.promoteServers ?? [])).map((server): StoreServerItem => ({ + ip: server.ip, + versionOverride: server.version, + description: server.description, + isRecommended: true + })) + ].map((server, index) => ({ ...server, index })), [serversListProvided]) // by lastJoined // const serversListSorted = useMemo(() => { // return serversList.map((server, index) => ({ ...server, index })).sort((a, b) => (b.lastJoined ?? 0) - (a.lastJoined ?? 0)) @@ -186,7 +207,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL }, [isEditScreenModal]) useCopyKeybinding(() => { - const item = serversListDisplay[selectedIndex] + const item = serversListProvided[selectedIndex] if (!item) return let str = `${item.ip}` if (item.versionOverride) { @@ -209,7 +230,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL if (!serverEditScreen) return if (serverEditScreen === true) { const server: StoreServerItem = { ...info, lastJoined: Date.now() } // so it appears first - appStorage.serversList = [server, ...(appStorage.serversList ?? serversListDisplay)] + appStorage.serversList = [server, ...(appStorage.serversList ?? serversListProvided)] } else { const index = appStorage.serversList?.indexOf(serverEditScreen) if (index !== undefined) { @@ -266,7 +287,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL overrides = server } - const lastJoinedUsername = serversListSorted.find(s => s.usernameOverride)?.usernameOverride + const lastJoinedUsername = serversListProvided.find(s => s.usernameOverride)?.usernameOverride let username = overrides.usernameOverride || getCurrentUsername() || '' if (!username) { const promptUsername = prompt('Enter username', lastJoinedUsername || '') @@ -290,7 +311,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL authenticatedAccount, saveServerToHistory: shouldSave, onSuccessfulPlay () { - if (shouldSave && !serversListDisplay.some(s => s.ip === ip)) { + if (shouldSave !== false && !serversListProvided.some(s => s.ip === ip)) { const newServersList: StoreServerItem[] = [ { ip, @@ -298,7 +319,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL versionOverride: overrides.versionOverride, numConnects: 1 }, - ...serversListDisplay + ...serversListProvided ] setNewServersList(newServersList) miscUiState.loadedServerIndex = (newServersList.length - 1).toString() @@ -306,10 +327,10 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL if (shouldSave === undefined) { // loading saved // find and update - const server = serversListDisplay.find(s => s.ip === ip) + const server = serversListProvided.find(s => s.ip === ip) if (server) { // move to top - const newList = [...serversListDisplay] + const newList = [...serversListProvided] const index = newList.indexOf(server) const thisItem = newList[index] newList.splice(index, 1) @@ -321,7 +342,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL } } }, - serverIndex: shouldSave ? serversListDisplay.length.toString() : indexOrIp // assume last + serverIndex: shouldSave ? serversListProvided.length.toString() : indexOrIp // assume last } satisfies ConnectOptions dispatchEvent(new CustomEvent('connect', { detail: options })) // qsOptions @@ -334,7 +355,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL appStorage.authenticatedAccounts = authenticatedAccounts.filter(a => a.username !== username) }} onWorldAction={(action, index) => { - const server = serversListDisplay[index] + const server = serversListProvided[index] if (!server) return if (action === 'edit') { @@ -386,6 +407,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL offline: additional?.offline, afterTitleUi: ( <Button + tabIndex={-1} icon="external-link" style={{ marginRight: 8, width: 20, height: 20 }} onClick={(e) => { @@ -394,14 +416,15 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL }} /> ), - group: customServersList ? 'Provided Servers' : 'Saved Servers' + group: customServersList ? 'Provided Servers' : (server.isRecommended ? '⭐️ Recommended Servers' : '💾 Saved Servers') } })} hidden={hidden} - onRowSelect={(_, i) => { - setSelectedIndex(i) + onRowSelect={(serverIndex) => { + setSelectedIndex(Number(serverIndex)) }} selectedRow={selectedIndex} + retriggerFocusCounter={retriggerFocusCounter} /> return <> {serversListJsx} diff --git a/src/react/Singleplayer.tsx b/src/react/Singleplayer.tsx index 5e531076..872a21ce 100644 --- a/src/react/Singleplayer.tsx +++ b/src/react/Singleplayer.tsx @@ -139,6 +139,7 @@ interface Props { setListHovered?: (hovered: boolean) => void secondRowStyles?: React.CSSProperties lockedEditing?: boolean + retriggerFocusCounter?: number } export default ({ @@ -163,7 +164,8 @@ export default ({ listStyle, setListHovered, secondRowStyles, - lockedEditing + lockedEditing, + retriggerFocusCounter }: Props) => { const containerRef = useRef<any>() const firstButton = useRef<HTMLButtonElement>(null) @@ -173,7 +175,7 @@ export default ({ if ((e.code === 'ArrowDown' || e.code === 'ArrowUp')) { e.preventDefault() const dir = e.code === 'ArrowDown' ? 1 : -1 - const elements = focusable(containerRef.current) + const elements = focusable(containerRef.current).filter(e => e.getAttribute('tabindex') !== '-1') const focusedElemIndex = elements.indexOf(document.activeElement as HTMLElement) if (focusedElemIndex === -1) return const nextElem = elements[focusedElemIndex + dir] @@ -196,7 +198,7 @@ export default ({ if (worldName) { worldRefs.current[worldName]?.focus() } - }, [selectedRow, worldData?.[selectedRow as any]?.name]) + }, [selectedRow, worldData?.[selectedRow as any]?.name, retriggerFocusCounter]) const onRowSelectHandler = (name: string, index: number) => { onRowSelect?.(name, index) @@ -263,22 +265,26 @@ export default ({ expanded={expandedGroups[groupName] ?? true} onToggle={() => toggleGroup(groupName)} /> - {(expandedGroups[groupName] ?? true) && worlds.map(({ name, size, detail, ...rest }, index) => ( - <World - {...rest} - size={size} - name={name} - elemRef={el => { worldRefs.current[name] = el }} - onFocus={row => onRowSelectHandler(row, index)} - isFocused={focusedWorld === name} - key={name} - onInteraction={(interaction) => { - if (interaction === 'enter') onWorldAction('load', name) - else if (interaction === 'space') firstButton.current?.focus() - }} - detail={detail} - /> - ))} + {(expandedGroups[groupName] ?? true) && worlds.map(({ name, size, detail, ...rest }, index) => { + const key = name + return ( + <World + data-key={key} + key={key} + {...rest} + size={size} + name={name} + elemRef={el => { worldRefs.current[name] = el }} + onFocus={row => onRowSelectHandler(row, index)} + isFocused={focusedWorld === name} + onInteraction={(interaction) => { + if (interaction === 'enter') onWorldAction('load', name) + else if (interaction === 'space') firstButton.current?.focus() + }} + detail={detail} + /> + ) + })} </React.Fragment> )) })() diff --git a/src/react/appStorageProvider.ts b/src/react/appStorageProvider.ts index 32617426..78852a8e 100644 --- a/src/react/appStorageProvider.ts +++ b/src/react/appStorageProvider.ts @@ -28,6 +28,7 @@ export interface StoreServerItem extends BaseServerInfo { optionsOverride?: Record<string, any> autoLogin?: Record<string, string> numConnects?: number // Track number of connections + isRecommended?: boolean } type StorageData = { diff --git a/src/react/serversStorage.ts b/src/react/serversStorage.ts index 9b095454..da7569cc 100644 --- a/src/react/serversStorage.ts +++ b/src/react/serversStorage.ts @@ -85,26 +85,6 @@ export const setNewServersList = (serversList: StoreServerItem[], force = false) appStorage.serversList = serversList } -export const getInitialServersList = () => { - // If we already have servers in appStorage, use those - if (appStorage.serversList) return appStorage.serversList - - const servers = [] as StoreServerItem[] - - if (servers.length === 0) { - // server list is empty, let's suggest some - for (const server of miscUiState.appConfig?.promoteServers ?? []) { - servers.push({ - ip: server.ip, - description: server.description, - versionOverride: server.version, - }) - } - } - - return servers -} - export const updateAuthenticatedAccountData = (callback: (data: AuthenticatedAccount[]) => AuthenticatedAccount[]) => { const accounts = appStorage.authenticatedAccounts const newAccounts = callback(accounts) From 243db1dc456a52cc72b10fd3a4b76ba41a6fdbe1 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Fri, 13 Jun 2025 04:56:40 +0300 Subject: [PATCH 023/181] use range generation instead --- config.json | 2 +- src/react/appStorageProvider.ts | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/config.json b/config.json index 8fd264fb..fe26a9e5 100644 --- a/config.json +++ b/config.json @@ -31,7 +31,7 @@ } ] ], - "defaultUsername": "mcrafter{num}{num}{num}{num}", + "defaultUsername": "mcrafter{0-9999}", "mobileButtons": [ { "action": "general.drop", diff --git a/src/react/appStorageProvider.ts b/src/react/appStorageProvider.ts index 78852a8e..499ec71c 100644 --- a/src/react/appStorageProvider.ts +++ b/src/react/appStorageProvider.ts @@ -93,7 +93,17 @@ export const setStorageDataOnAppConfigLoad = (appConfig: AppConfig) => { } export const getRandomUsername = (appConfig: AppConfig) => { - return appConfig.defaultUsername?.replaceAll('{num}', () => Math.floor(Math.random() * 10).toString()) ?? '' + if (!appConfig.defaultUsername) return '' + + const username = appConfig.defaultUsername + .replaceAll(/{(\d+)-(\d+)}/g, (_, start, end) => { + const min = Number(start) + const max = Number(end) + return Math.floor(Math.random() * (max - min + 1) + min).toString() + }) + .replaceAll('{num}', () => Math.floor(Math.random() * 10).toString()) + + return username } export const appStorage = proxy({ ...defaultStorageData }) From 1e513f87dd6c9f8b78d124a2a0b1c446bcd302bb Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Fri, 13 Jun 2025 05:23:58 +0300 Subject: [PATCH 024/181] feat: add End portal & gateway rendering --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 655cd394..3559169d 100644 --- a/package.json +++ b/package.json @@ -150,7 +150,7 @@ "http-browserify": "^1.7.0", "http-server": "^14.1.1", "https-browserify": "^1.0.0", - "mc-assets": "^0.2.54", + "mc-assets": "^0.2.59", "minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next", "mineflayer": "github:zardoy/mineflayer#gen-the-master", "mineflayer-mouse": "^0.1.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 60086040..63ee982e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -334,8 +334,8 @@ importers: specifier: ^1.0.0 version: 1.0.0 mc-assets: - specifier: ^0.2.54 - version: 0.2.54 + specifier: ^0.2.59 + version: 0.2.59 minecraft-inventory-gui: specifier: github:zardoy/minecraft-inventory-gui#next version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/f425e6a2db796d42cd2c09cf715dad841dec643d(@types/react@18.3.18)(react@18.3.1) @@ -6475,8 +6475,8 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - mc-assets@0.2.54: - resolution: {integrity: sha512-ZEaa9IcqfOt4cFGLVJVkZMemKPfbjQskvIxuDepkXWkJb9T+xQ+Hj86zDMh1Ah8WZWNeGx2x26CuXt8QNr6pcw==} + mc-assets@0.2.59: + resolution: {integrity: sha512-HGdy6v09X5nks8+NuwrL3KQ763D+eWFeSpWLXx3+doWz6hSEeLjgBPOrB1stvQOjPDiQCzsIv5gaRB5sl6ng1A==} engines: {node: '>=18.0.0'} mcraft-fun-mineflayer@0.1.23: @@ -17028,7 +17028,7 @@ snapshots: math-intrinsics@1.1.0: {} - mc-assets@0.2.54: + mc-assets@0.2.59: dependencies: maxrects-packer: '@zardoy/maxrects-packer@2.7.4' zod: 3.24.2 From b69813435c17b20964ff3ad8a98054a4e6ba4bbc Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Fri, 13 Jun 2025 08:06:09 +0300 Subject: [PATCH 025/181] up test --- renderer/viewer/lib/mesher/test/tests.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/renderer/viewer/lib/mesher/test/tests.test.ts b/renderer/viewer/lib/mesher/test/tests.test.ts index 9ebd6604..2c3dc6a5 100644 --- a/renderer/viewer/lib/mesher/test/tests.test.ts +++ b/renderer/viewer/lib/mesher/test/tests.test.ts @@ -49,8 +49,6 @@ test('Known blocks are not rendered', () => { // TODO resolve creaking_heart issue (1.21.3) expect(missingBlocks).toMatchInlineSnapshot(` { - "end_gateway": true, - "end_portal": true, "structure_void": true, } `) From a3dcfed4d0c3710a8e1273edb1ed85b4d4a033d4 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Fri, 13 Jun 2025 13:11:06 +0300 Subject: [PATCH 026/181] feat: add time and battery status that is displayed in fullscreen by default --- src/optionsStorage.ts | 1 + src/react/FullscreenTime.tsx | 112 +++++++++++++++++++++++++++++++++++ src/reactUi.tsx | 2 + 3 files changed, 115 insertions(+) create mode 100644 src/react/FullscreenTime.tsx diff --git a/src/optionsStorage.ts b/src/optionsStorage.ts index 59f222bd..882610f8 100644 --- a/src/optionsStorage.ts +++ b/src/optionsStorage.ts @@ -77,6 +77,7 @@ const defaultOptions = { debugResponseTimeIndicator: false, chatPingExtension: true, // antiAliasing: false, + topRightTimeDisplay: 'only-fullscreen' as 'only-fullscreen' | 'always' | 'never', clipWorldBelowY: undefined as undefined | number, // will be removed disableSignsMapsSupport: false, diff --git a/src/react/FullscreenTime.tsx b/src/react/FullscreenTime.tsx new file mode 100644 index 00000000..25bd12e4 --- /dev/null +++ b/src/react/FullscreenTime.tsx @@ -0,0 +1,112 @@ +import { useEffect, useState } from 'react' +import { useSnapshot } from 'valtio' +import { options } from '../optionsStorage' +import PixelartIcon, { pixelartIcons } from './PixelartIcon' + +interface BatteryManager extends EventTarget { + charging: boolean + chargingTime: number + dischargingTime: number + level: number + addEventListener(type: 'chargingchange' | 'levelchange', listener: EventListener): void + removeEventListener(type: 'chargingchange' | 'levelchange', listener: EventListener): void +} + +declare global { + interface Navigator { + getBattery(): Promise<BatteryManager> + } +} + +export default () => { + const [fullScreen, setFullScreen] = useState(false) + const { topRightTimeDisplay } = useSnapshot(options) + if (topRightTimeDisplay === 'never') return null + return <FullscreenTime /> +} + +const FullscreenTime = () => { + const { topRightTimeDisplay } = useSnapshot(options) + const [fullScreen, setFullScreen] = useState(false) + const [time, setTime] = useState('') + const [batteryInfo, setBatteryInfo] = useState<{ level: number, charging: boolean } | null>(null) + + useEffect(() => { + document.documentElement.addEventListener('fullscreenchange', () => { + setFullScreen(!!document.fullscreenElement) + }) + + // Update time every second + const updateTime = () => { + const now = new Date() + const hours = now.getHours().toString().padStart(2, '0') + const minutes = now.getMinutes().toString().padStart(2, '0') + setTime(`${hours}:${minutes}`) + } + updateTime() + const timeInterval = setInterval(updateTime, 1000) + + // Get battery info if available + if ('getBattery' in navigator) { + void navigator.getBattery().then(battery => { + const updateBatteryInfo = () => { + setBatteryInfo({ + level: Math.round(battery.level * 100), + charging: battery.charging + }) + } + updateBatteryInfo() + battery.addEventListener('levelchange', updateBatteryInfo) + battery.addEventListener('chargingchange', updateBatteryInfo) + return () => { + battery.removeEventListener('levelchange', updateBatteryInfo) + battery.removeEventListener('chargingchange', updateBatteryInfo) + } + }) + } + + return () => { + clearInterval(timeInterval) + } + }, []) + + if (topRightTimeDisplay === 'only-fullscreen' && !fullScreen) return null + + return ( + <div style={{ + position: 'fixed', + top: 'env(safe-area-inset-top, 5px)', + right: 'env(safe-area-inset-right, 5px)', + display: 'flex', + alignItems: 'center', + gap: '4px', + padding: '1px 3px', + background: 'rgba(0, 0, 0, 0.75)', + borderRadius: '2px', + fontSize: 8, + color: 'white', + fontFamily: 'minecraft, mojangles, monospace', + zIndex: 1, + pointerEvents: 'none' + }}> + <span>{time}</span> + {batteryInfo && ( + <div style={{ display: 'flex', alignItems: 'center', gap: '2px' }}> + <PixelartIcon + iconName={getBatteryIcon(batteryInfo.level, batteryInfo.charging)} + styles={{ fontSize: 12 }} + /> + <span>{batteryInfo.level}%</span> + </div> + )} + </div> + ) +} + +const getBatteryIcon = (level: number, charging: boolean) => { + if (charging) return pixelartIcons['battery-charging'] + if (level > 60) return pixelartIcons['battery-full'] + if (level > 20) return pixelartIcons['battery-2'] + if (level > 5) return pixelartIcons['battery-1'] + return pixelartIcons['battery'] +} diff --git a/src/reactUi.tsx b/src/reactUi.tsx index e67facea..fa67d692 100644 --- a/src/reactUi.tsx +++ b/src/reactUi.tsx @@ -64,6 +64,7 @@ import DebugResponseTimeIndicator from './react/debugs/DebugResponseTimeIndicato import RendererDebugMenu from './react/RendererDebugMenu' import CreditsAboutModal from './react/CreditsAboutModal' import GlobalOverlayHints from './react/GlobalOverlayHints' +import FullscreenTime from './react/FullscreenTime' const isFirefox = ua.getBrowser().name === 'Firefox' if (isFirefox) { @@ -174,6 +175,7 @@ const InGameUi = () => { <PerComponentErrorBoundary> <PauseScreen /> + <FullscreenTime /> <MineflayerPluginHud /> <MineflayerPluginConsole /> {showUI && <TouchInteractionHint />} From 794cafb1f6d3fa6a43b4f84cb4633b64f4f42ab3 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Fri, 13 Jun 2025 13:22:15 +0300 Subject: [PATCH 027/181] feat: add useful entities debug entry to F3 --- renderer/viewer/three/entities.ts | 10 +++++++ renderer/viewer/three/graphicsBackend.ts | 3 ++ src/react/FullscreenTime.tsx | 37 +++++++++++++----------- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/renderer/viewer/three/entities.ts b/renderer/viewer/three/entities.ts index 1a5e7c3f..8a270332 100644 --- a/renderer/viewer/three/entities.ts +++ b/renderer/viewer/three/entities.ts @@ -237,6 +237,16 @@ export class Entities { return Object.values(this.entities).filter(entity => entity.visible).length } + getDebugString (): string { + const totalEntities = Object.keys(this.entities).length + const visibleEntities = this.entitiesRenderingCount + + const playerEntities = Object.values(this.entities).filter(entity => entity.playerObject) + const visiblePlayerEntities = playerEntities.filter(entity => entity.visible) + + return `${visibleEntities}/${totalEntities} ${visiblePlayerEntities.length}/${playerEntities.length}` + } + constructor (public worldRenderer: WorldRendererThree) { this.debugMode = 'none' this.onSkinUpdate = () => { } diff --git a/renderer/viewer/three/graphicsBackend.ts b/renderer/viewer/three/graphicsBackend.ts index 37acfde9..01bb7c56 100644 --- a/renderer/viewer/three/graphicsBackend.ts +++ b/renderer/viewer/three/graphicsBackend.ts @@ -124,6 +124,9 @@ const createGraphicsBackend: GraphicsBackendLoader = (initOptions: GraphicsInitO if (worldRenderer) worldRenderer.renderingActive = rendering }, getDebugOverlay: () => ({ + get entitiesString () { + return worldRenderer?.entities.getDebugString() + }, }), updateCamera (pos: Vec3 | null, yaw: number, pitch: number) { worldRenderer?.setFirstPersonCamera(pos, yaw, pitch) diff --git a/src/react/FullscreenTime.tsx b/src/react/FullscreenTime.tsx index 25bd12e4..00b56740 100644 --- a/src/react/FullscreenTime.tsx +++ b/src/react/FullscreenTime.tsx @@ -73,28 +73,31 @@ const FullscreenTime = () => { if (topRightTimeDisplay === 'only-fullscreen' && !fullScreen) return null return ( - <div style={{ - position: 'fixed', - top: 'env(safe-area-inset-top, 5px)', - right: 'env(safe-area-inset-right, 5px)', - display: 'flex', - alignItems: 'center', - gap: '4px', - padding: '1px 3px', - background: 'rgba(0, 0, 0, 0.75)', - borderRadius: '2px', - fontSize: 8, - color: 'white', - fontFamily: 'minecraft, mojangles, monospace', - zIndex: 1, - pointerEvents: 'none' - }}> + <div + className='top-right-time' + style={{ + position: 'fixed', + top: 'env(safe-area-inset-top, 5px)', + right: 'env(safe-area-inset-right, 5px)', + display: 'flex', + alignItems: 'center', + gap: '4px', + padding: '1px 3px', + background: 'rgba(0, 0, 0, 0.75)', + borderRadius: '2px', + fontSize: 8, + color: 'white', + fontFamily: 'minecraft, mojangles, monospace', + zIndex: 1, + pointerEvents: 'none' + }} + > <span>{time}</span> {batteryInfo && ( <div style={{ display: 'flex', alignItems: 'center', gap: '2px' }}> <PixelartIcon iconName={getBatteryIcon(batteryInfo.level, batteryInfo.charging)} - styles={{ fontSize: 12 }} + styles={{ fontSize: 10 }} /> <span>{batteryInfo.level}%</span> </div> From 9f3079b5f5d94ad3162c0f297fab745a79e33f1f Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Fri, 13 Jun 2025 14:14:08 +0300 Subject: [PATCH 028/181] feat: rework effects display with new UI! fix a few related bugs --- src/optionsGuiScheme.tsx | 9 +- src/react/IndicatorEffects.css | 63 +++++++++++-- src/react/IndicatorEffects.stories.tsx | 33 ------- src/react/IndicatorEffects.tsx | 118 ++++++++++++++----------- src/react/IndicatorEffectsProvider.tsx | 83 +++++++++-------- src/reactUi.tsx | 2 +- 6 files changed, 174 insertions(+), 134 deletions(-) delete mode 100644 src/react/IndicatorEffects.stories.tsx diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx index e3b71778..3409dc76 100644 --- a/src/optionsGuiScheme.tsx +++ b/src/optionsGuiScheme.tsx @@ -365,7 +365,12 @@ export const guiOptionsScheme: { }, { custom () { - return <UiToggleButton name='effects-indicators' label='Effects & Indicators' /> + return <UiToggleButton name='effects' label='Effects' /> + }, + }, + { + custom () { + return <UiToggleButton name='indicators' label='Game Indicators' /> }, }, { @@ -717,7 +722,7 @@ const Category = ({ children }) => <div style={{ gridColumn: 'span 2' }}>{children}</div> -const UiToggleButton = ({ name, addUiText = false, label = noCase(name) }) => { +const UiToggleButton = ({ name, addUiText = false, label = noCase(name) }: { name: string, addUiText?: boolean, label?: string }) => { const { disabledUiParts } = useSnapshot(options) const currentlyDisabled = disabledUiParts.includes(name) diff --git a/src/react/IndicatorEffects.css b/src/react/IndicatorEffects.css index 6b178c58..797b37e8 100644 --- a/src/react/IndicatorEffects.css +++ b/src/react/IndicatorEffects.css @@ -1,9 +1,9 @@ -.effectsScreen-container { +.indicators-container-outer { position: fixed; top: max(6%, 30px); - left: 0px; + left: calc(env(safe-area-inset-left) / 2); z-index: -2; - pointer-events: none; + /* pointer-events: none; */ } .indicators-container { @@ -17,19 +17,68 @@ } .effect-box { + position: relative; display: flex; align-items: center; + background: rgba(40, 40, 40, 0.85); + border-top: 1px solid #9f9f9f; + border-left: 1px solid #9f9f9f; + border-right: 1px solid #373737; + border-bottom: 1px solid #373737; + padding: 3px 9px 3px 4px; + margin-bottom: 3px; + min-width: 60px; + min-height: 22px; + box-sizing: border-box; + overflow: hidden; +} + +.effect-box__progress-bg { + position: absolute; + left: 0; + top: 0; + bottom: 0; + background: linear-gradient(90deg, #b2b2b2 0%, #6b6b6b 100%); + opacity: 0.18; + z-index: 0; + transition: width 0.3s linear; } .effect-box__image { - width: 23px; - margin-right: 3px; + width: 16px; + height: 16px; + margin-right: 4px; + z-index: 1; +} + +.effect-box__content { + display: flex; + flex-direction: column; + justify-content: center; + z-index: 1; + flex: 1; +} + +.effect-box__title { + color: #e0e0e0; + font-size: 0.45rem; + font-weight: 600; + margin-bottom: 1px; + text-shadow: 1px 1px 0 #222, 0 0 2px #000; } .effect-box__time { - font-size: 0.65rem; + color: #fff; + font-size: 0.5rem; + text-shadow: 1px 1px 0 #222, 0 0 2px #000; } .effect-box__level { - font-size: 0.45rem; + position: absolute; + right: 4px; + top: 4px; + color: #fff; + font-size: 0.48rem; + text-shadow: 1px 1px 0 #222, 0 0 2px #000; + z-index: 2; } diff --git a/src/react/IndicatorEffects.stories.tsx b/src/react/IndicatorEffects.stories.tsx deleted file mode 100644 index e0371550..00000000 --- a/src/react/IndicatorEffects.stories.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react' - -import IndicatorEffects, { defaultIndicatorsState } from './IndicatorEffects' -import { images } from './effectsImages' - -const meta: Meta<typeof IndicatorEffects> = { - component: IndicatorEffects -} - -export default meta -type Story = StoryObj<typeof IndicatorEffects> - -export const Primary: Story = { - args: { - indicators: defaultIndicatorsState, - effects: [ - { - image: images.glowing, - time: 200, - level: 255, - removeEffect (image: string) { }, - reduceTime (image: string) { } - }, - { - image: images.absorption, - time: 30, - level: 99, - removeEffect (image: string) { }, - reduceTime (image: string) { } - } - ], - } -} diff --git a/src/react/IndicatorEffects.tsx b/src/react/IndicatorEffects.tsx index 5b05290f..50ece8d5 100644 --- a/src/react/IndicatorEffects.tsx +++ b/src/react/IndicatorEffects.tsx @@ -1,4 +1,4 @@ -import { useMemo, useEffect, useRef } from 'react' +import { useMemo, useEffect, useRef, useState } from 'react' import PixelartIcon, { pixelartIcons } from './PixelartIcon' import './IndicatorEffects.css' @@ -9,35 +9,55 @@ function formatTime (seconds: number): string { const minutes = Math.floor(seconds / 60) const remainingSeconds = Math.floor(seconds % 60) const formattedMinutes = String(minutes).padStart(2, '0') - const formattedSeconds = String(remainingSeconds) + const formattedSeconds = String(remainingSeconds).padStart(2, '0') return `${formattedMinutes}:${formattedSeconds}` } export type EffectType = { + id: number, image: string, - time: number, level: number, - removeEffect: (image: string) => void, - reduceTime: (image: string) => void + initialTime: number, + duration: number, + name: string, } -const EffectBox = ({ image, time, level }: Pick<EffectType, 'image' | 'time' | 'level'>) => { +const EffectBox = ({ image, level, name, initialTime, duration }: Pick<EffectType, 'image' | 'level' | 'initialTime' | 'name' | 'duration'>) => { + const [currentTime, setCurrentTime] = useState(Date.now()) - const formattedTime = useMemo(() => formatTime(time), [time]) + useEffect(() => { + const interval = setInterval(() => { + setCurrentTime(Date.now()) + }, 100) + return () => clearInterval(interval) + }, []) - return <div className='effect-box'> - <img className='effect-box__image' src={image} alt='' /> - <div> - {formattedTime ? ( - // if time is negative then effect is shown without time. - // Component should be removed manually with time = 0 - <div className='effect-box__time'>{formattedTime}</div> - ) : null} - {level > 0 && level < 256 ? ( - <div className='effect-box__level'>{level + 1}</div> - ) : null} + const timeElapsed = (currentTime - initialTime) / 1000 + const timeRemaining = Math.max(0, duration - timeElapsed) + const progress = duration > 0 ? Math.max(0, Math.min(1, timeRemaining / duration)) : 0 + const formattedTime = useMemo(() => formatTime(timeRemaining), [timeRemaining]) + + // Convert level to Roman numerals + const toRomanNumeral = (num: number): string => { + if (num <= 0) return '' + const romanNumerals = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X'] + return romanNumerals[num - 1] || `${num}` + } + + const levelText = level > 0 && level < 256 ? ` ${toRomanNumeral(level + 1)}` : '' + + return ( + <div className='effect-box'> + <div className='effect-box__progress-bg' style={{ width: `${progress * 100}%` }} /> + <img className='effect-box__image' src={image} alt='' /> + <div className='effect-box__content'> + <div className='effect-box__title'>{name}{levelText}</div> + {formattedTime && ( + <div className='effect-box__time'>{formattedTime}</div> + )} + </div> </div> - </div> + ) } export const defaultIndicatorsState = { @@ -68,29 +88,17 @@ const colorOverrides = { } } -export default ({ indicators, effects }: { indicators: typeof defaultIndicatorsState, effects: readonly EffectType[] }) => { - const effectsRef = useRef(effects) - useEffect(() => { - effectsRef.current = effects - }, [effects]) - - useEffect(() => { - // todo use more precise timer for each effect - const interval = setInterval(() => { - for (const [index, effect] of effectsRef.current.entries()) { - if (effect.time === 0) { - // effect.removeEffect(effect.image) - return - } - effect.reduceTime(effect.image) - } - }, 1000) - - return () => { - clearInterval(interval) - } - }, []) - +export default ({ + indicators, + effects, + displayIndicators, + displayEffects +}: { + indicators: typeof defaultIndicatorsState, + effects: readonly EffectType[] + displayIndicators: boolean + displayEffects: boolean +}) => { const indicatorsMapped = Object.entries(defaultIndicatorsState).map(([key]) => { const state = indicators[key] return { @@ -100,10 +108,10 @@ export default ({ indicators, effects }: { indicators: typeof defaultIndicatorsS key } }) - return <div className='effectsScreen-container'> + return <div className='indicators-container-outer'> <div className='indicators-container'> { - indicatorsMapped.map((indicator) => <div + displayIndicators && indicatorsMapped.map((indicator) => <div key={indicator.icon} style={{ opacity: indicator.state ? 1 : 0, @@ -115,15 +123,17 @@ export default ({ indicators, effects }: { indicators: typeof defaultIndicatorsS </div>) } </div> - <div className='effects-container'> - { - effects.map((effect) => <EffectBox - key={`effectBox-${effect.image}`} - image={effect.image} - time={effect.time} - level={effect.level} - />) - } - </div> + {displayEffects && <EffectsInner effects={effects} />} + </div> +} + +const EffectsInner = ({ effects }: { effects: readonly EffectType[] }) => { + return <div className='effects-container'> + {effects.map((effect) => ( + <EffectBox + key={`effectBox-${effect.id}`} + {...effect} + /> + ))} </div> } diff --git a/src/react/IndicatorEffectsProvider.tsx b/src/react/IndicatorEffectsProvider.tsx index 097bb28d..c479d7e1 100644 --- a/src/react/IndicatorEffectsProvider.tsx +++ b/src/react/IndicatorEffectsProvider.tsx @@ -1,6 +1,7 @@ import { proxy, subscribe, useSnapshot } from 'valtio' import { useEffect, useMemo, useState } from 'react' import { subscribeKey } from 'valtio/utils' +import { Effect } from 'mineflayer' import { inGameError } from '../utils' import { fsState } from '../loadSave' import { gameAdditionalState, miscUiState } from '../globalState' @@ -14,43 +15,52 @@ export const state = proxy({ effects: [] as EffectType[] }) -export const addEffect = (newEffect: Omit<EffectType, 'reduceTime' | 'removeEffect'>) => { - const effectIndex = getEffectIndex(newEffect as EffectType) +export const addEffect = (newEffect: Effect) => { + const effectData = loadedData.effectsArray.find(e => e.id === newEffect.id) + const name = effectData?.name ?? `unknown: ${newEffect.id}` + const nameKebab = name.replaceAll(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`).slice(1) + const image = images[nameKebab] ?? null + if (!image) { + inGameError(`received unknown effect id ${newEffect.id}`) + return + } + + const effectIndex = getEffectIndex({ id: newEffect.id }) if (typeof effectIndex === 'number') { - state.effects[effectIndex].time = newEffect.time - state.effects[effectIndex].level = newEffect.level + state.effects[effectIndex].initialTime = Date.now() + state.effects[effectIndex].level = newEffect.amplifier + state.effects[effectIndex].duration = newEffect.duration / 20 // convert ticks to seconds } else { - const effect = { ...newEffect, reduceTime, removeEffect } + const effect: EffectType = { + id: newEffect.id, + name, + image, + level: newEffect.amplifier, + initialTime: Date.now(), + duration: newEffect.duration / 20, // convert ticks to seconds + } state.effects.push(effect) } } -const removeEffect = (image: string) => { +const removeEffect = (id: number) => { for (const [index, effect] of (state.effects).entries()) { - if (effect.image === image) { + if (effect.id === id) { state.effects.splice(index, 1) } } } -const reduceTime = (image: string) => { +const getEffectIndex = (newEffect: Pick<EffectType, 'id'>) => { for (const [index, effect] of (state.effects).entries()) { - if (effect.image === image) { - effect.time -= 1 - } - } -} - -const getEffectIndex = (newEffect: EffectType) => { - for (const [index, effect] of (state.effects).entries()) { - if (effect.image === newEffect.image) { + if (effect.id === newEffect.id) { return index } } return null } -export default () => { +export default ({ displayEffects = true, displayIndicators = true }: { displayEffects?: boolean, displayIndicators?: boolean }) => { const [dummyState, setDummyState] = useState(false) const stateIndicators = useSnapshot(state.indicators) const chunksLoading = !useSnapshot(appViewer.rendererState).world.allChunksLoaded @@ -87,33 +97,32 @@ export default () => { const nameKebab = effect.name.replaceAll(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`).slice(1) return [effect.id, images[nameKebab]] })) - bot.on('entityEffect', (entity, effect) => { + const gotEffect = (entity: import('prismarine-entity').Entity, effect: Effect) => { if (entity.id !== bot.entity.id) return - const image = effectsImages[effect.id] ?? null - if (!image) { - inGameError(`received unknown effect id ${effect.id}}`) - return - } - const newEffect = { - image, - time: effect.duration / 20, // duration received in ticks - level: effect.amplifier, - } - addEffect(newEffect) - }) + addEffect(effect) + } + bot.on('entityEffect', gotEffect) + + // gotEffect(bot.entity, { + // id: 1, + // amplifier: 1, + // duration: 100, + // }) + + for (const effect of Object.values(bot.entity.effects ?? {})) { + gotEffect(bot.entity, effect) + } + bot.on('entityEffectEnd', (entity, effect) => { if (entity.id !== bot.entity.id) return - const image = effectsImages[effect.id] ?? null - if (!image) { - inGameError(`received unknown effect id ${effect.id}}}`) - return - } - removeEffect(image) + removeEffect(effect.id) }) }, []) return <IndicatorEffects indicators={allIndicators} effects={effects} + displayIndicators + displayEffects /> } diff --git a/src/reactUi.tsx b/src/reactUi.tsx index fa67d692..138696b0 100644 --- a/src/reactUi.tsx +++ b/src/reactUi.tsx @@ -163,7 +163,7 @@ const InGameUi = () => { {showMinimap !== 'never' && <MinimapProvider adapter={adapter} displayMode='minimapOnly' />} {!disabledUiParts.includes('title') && <TitleProvider />} {!disabledUiParts.includes('scoreboard') && <ScoreboardProvider />} - {!disabledUiParts.includes('effects-indicators') && <IndicatorEffectsProvider />} + <IndicatorEffectsProvider displayEffects={!disabledUiParts.includes('effects')} displayIndicators={!disabledUiParts.includes('indicators')} /> {!disabledUiParts.includes('crosshair') && <Crosshair />} {!disabledUiParts.includes('books') && <BookProvider />} {!disabledUiParts.includes('bossbars') && displayBossBars && <BossBarOverlayProvider />} From 8c71f70db2f04402cead0732886fde3ec4cfdcdb Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Fri, 13 Jun 2025 15:12:58 +0300 Subject: [PATCH 029/181] fix: fix shifting didn't work on some servers after check --- pnpm-lock.yaml | 147 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 129 insertions(+), 18 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 63ee982e..60c25f84 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -136,7 +136,7 @@ importers: version: 4.17.21 mcraft-fun-mineflayer: specifier: ^0.1.23 - version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/e4818de9e1ba2783a930b4358499ac4033e6383d(encoding@0.1.13)) + version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/45f29f9fd1ad6d43d76bbd0b196f2ed4e675772b(encoding@0.1.13)) minecraft-data: specifier: 3.89.0 version: 3.89.0 @@ -341,7 +341,7 @@ importers: version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/f425e6a2db796d42cd2c09cf715dad841dec643d(@types/react@18.3.18)(react@18.3.1) mineflayer: specifier: github:zardoy/mineflayer#gen-the-master - version: https://codeload.github.com/zardoy/mineflayer/tar.gz/e4818de9e1ba2783a930b4358499ac4033e6383d(encoding@0.1.13) + version: https://codeload.github.com/zardoy/mineflayer/tar.gz/45f29f9fd1ad6d43d76bbd0b196f2ed4e675772b(encoding@0.1.13) mineflayer-mouse: specifier: ^0.1.10 version: 0.1.10(@types/debug@4.1.12)(@types/node@22.13.9)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) @@ -3614,6 +3614,10 @@ packages: resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} engines: {node: '>= 0.4'} + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -4773,6 +4777,10 @@ packages: resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==} engines: {node: '>= 0.4'} + es-abstract@1.24.0: + resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} + engines: {node: '>= 0.4'} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -5949,6 +5957,10 @@ packages: resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} engines: {node: '>= 0.4'} + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + is-number-object@1.1.1: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} @@ -6724,8 +6736,8 @@ packages: resolution: {integrity: sha512-3bxph4jfbkBh5HpeouorxzrfSLNV+i+1gugNJ2jf52HW+rt+tW7eiiFPxrJEsOVkPT/3O/dEIW7j93LRlojMkQ==} engines: {node: '>=22'} - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/e4818de9e1ba2783a930b4358499ac4033e6383d: - resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/e4818de9e1ba2783a930b4358499ac4033e6383d} + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/45f29f9fd1ad6d43d76bbd0b196f2ed4e675772b: + resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/45f29f9fd1ad6d43d76bbd0b196f2ed4e675772b} version: 4.27.0 engines: {node: '>=22'} @@ -8449,9 +8461,17 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@3.8.1: resolution: {integrity: sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA==} + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + store2@2.14.4: resolution: {integrity: sha512-srTItn1GOvyvOycgxjAnPA63FZNwy0PTyUBFMHRM+hVFltAeoh0LmNBz9SZqUS9mMqGk8rfyWyXn3GH5ReJ8Zw==} @@ -9404,6 +9424,10 @@ packages: resolution: {integrity: sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==} engines: {node: '>= 0.4'} + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} hasBin: true @@ -9751,7 +9775,7 @@ snapshots: '@babel/core': 7.26.9 '@babel/helper-compilation-targets': 7.26.5 '@babel/helper-plugin-utils': 7.26.5 - debug: 4.4.1 + debug: 4.4.0(supports-color@8.1.1) lodash.debounce: 4.0.8 resolve: 1.22.10 transitivePeerDependencies: @@ -13322,7 +13346,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.1 + debug: 4.4.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color optional: true @@ -13452,6 +13476,17 @@ snapshots: get-intrinsic: 1.3.0 is-string: 1.1.1 + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + array-union@2.1.0: {} array-unique@0.3.2: {} @@ -13460,7 +13495,7 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 es-errors: 1.3.0 es-object-atoms: 1.1.1 es-shim-unscopables: 1.1.0 @@ -13483,7 +13518,7 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 es-errors: 1.3.0 es-shim-unscopables: 1.1.0 @@ -14939,6 +14974,63 @@ snapshots: unbox-primitive: 1.1.0 which-typed-array: 1.1.18 + es-abstract@1.24.0: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.19 + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -14948,7 +15040,7 @@ snapshots: call-bind: 1.0.8 call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 es-errors: 1.3.0 es-set-tostringtag: 2.1.0 function-bind: 1.1.2 @@ -15219,7 +15311,7 @@ snapshots: eslint-plugin-react@7.37.4(eslint@8.57.1): dependencies: - array-includes: 3.1.8 + array-includes: 3.1.9 array.prototype.findlast: 1.2.5 array.prototype.flatmap: 1.3.3 array.prototype.tosorted: 1.1.4 @@ -16428,6 +16520,8 @@ snapshots: call-bind: 1.0.8 define-properties: 1.2.1 + is-negative-zero@2.0.3: {} + is-number-object@1.1.1: dependencies: call-bound: 1.0.4 @@ -16738,7 +16832,7 @@ snapshots: jsx-ast-utils@3.3.5: dependencies: - array-includes: 3.1.8 + array-includes: 3.1.9 array.prototype.flat: 1.3.3 object.assign: 4.1.7 object.values: 1.2.1 @@ -17033,12 +17127,12 @@ snapshots: maxrects-packer: '@zardoy/maxrects-packer@2.7.4' zod: 3.24.2 - mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/e4818de9e1ba2783a930b4358499ac4033e6383d(encoding@0.1.13)): + mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/45f29f9fd1ad6d43d76bbd0b196f2ed4e675772b(encoding@0.1.13)): dependencies: '@zardoy/flying-squid': 0.0.49(encoding@0.1.13) exit-hook: 2.2.1 minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=1546deaf50efae3d6564fcc9f08da99d3ae8096ac98f420b87b48b8c9501fd37)(encoding@0.1.13) - mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/e4818de9e1ba2783a930b4358499ac4033e6383d(encoding@0.1.13) + mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/45f29f9fd1ad6d43d76bbd0b196f2ed4e675772b(encoding@0.1.13) prismarine-item: 1.16.0 ws: 8.18.1 transitivePeerDependencies: @@ -17268,7 +17362,7 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.12 - debug: 4.4.1 + debug: 4.4.0(supports-color@8.1.1) decode-named-character-reference: 1.1.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -17475,7 +17569,7 @@ snapshots: - encoding - supports-color - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/e4818de9e1ba2783a930b4358499ac4033e6383d(encoding@0.1.13): + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/45f29f9fd1ad6d43d76bbd0b196f2ed4e675772b(encoding@0.1.13): dependencies: '@nxg-org/mineflayer-physics-util': 1.8.10 minecraft-data: 3.89.0 @@ -17882,7 +17976,7 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 es-object-atoms: 1.1.1 object.pick@1.3.0: @@ -19204,7 +19298,7 @@ snapshots: ms: 2.1.3 on-finished: 2.4.1 range-parser: 1.2.1 - statuses: 2.0.1 + statuses: 2.0.2 transitivePeerDependencies: - supports-color @@ -19669,8 +19763,15 @@ snapshots: statuses@2.0.1: {} + statuses@2.0.2: {} + std-env@3.8.1: {} + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + store2@2.14.4: {} storybook@7.6.20(encoding@0.1.13): @@ -19740,7 +19841,7 @@ snapshots: string.prototype.repeat@1.0.0: dependencies: define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 string.prototype.trim@1.2.10: dependencies: @@ -20684,6 +20785,16 @@ snapshots: gopd: 1.2.0 has-tostringtag: 1.0.2 + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + which@1.3.1: dependencies: isexe: 2.0.0 From 679c3775f7a85962e6c0b5e8e78ce6caf9a7b0d7 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Sun, 15 Jun 2025 16:52:24 +0300 Subject: [PATCH 030/181] feat: convert formatted text color to display p3 display so its more vibrant on macbooks with xdr display and other p3 monitors --- src/chatUtils.ts | 4 +++ src/react/MessageFormatted.css | 2 +- src/react/MessageFormatted.tsx | 45 ++++++++++++++-------------- src/react/MessageFormattedString.tsx | 7 +++-- src/react/Scoreboard.css | 2 +- 5 files changed, 33 insertions(+), 27 deletions(-) diff --git a/src/chatUtils.ts b/src/chatUtils.ts index 384dbdae..88437bc3 100644 --- a/src/chatUtils.ts +++ b/src/chatUtils.ts @@ -4,6 +4,10 @@ import { fromFormattedString, TextComponent } from '@xmcl/text-component' import type { IndexedData } from 'minecraft-data' import { versionToNumber } from 'renderer/viewer/common/utils' +export interface MessageFormatOptions { + doShadow?: boolean +} + export type MessageFormatPart = Pick<TextComponent, 'hoverEvent' | 'clickEvent'> & { text: string color?: string diff --git a/src/react/MessageFormatted.css b/src/react/MessageFormatted.css index 7e5be00a..f16a0644 100644 --- a/src/react/MessageFormatted.css +++ b/src/react/MessageFormatted.css @@ -1,5 +1,5 @@ /* base global styles */ .formatted-message { - text-shadow: 1px 1px 0px #3f3f3f;; + /* text-shadow: 1px 1px 0px #3f3f3f; */ } diff --git a/src/react/MessageFormatted.tsx b/src/react/MessageFormatted.tsx index 554d5a9b..a70ca3b1 100644 --- a/src/react/MessageFormatted.tsx +++ b/src/react/MessageFormatted.tsx @@ -3,7 +3,7 @@ import { render } from '@xmcl/text-component' import { noCase } from 'change-case' import mojangson from 'mojangson' import { openURL } from 'renderer/viewer/lib/simpleUtils' -import { MessageFormatPart } from '../chatUtils' +import { MessageFormatOptions, MessageFormatPart } from '../chatUtils' import { chatInputValueGlobal } from './Chat' import './MessageFormatted.css' import { showOptionsModal } from './SelectOption' @@ -67,9 +67,10 @@ const clickEventToProps = (clickEvent: MessageFormatPart['clickEvent']) => { } } -export const MessagePart = ({ part, ...props }: { part: MessageFormatPart } & ComponentProps<'span'>) => { +export const MessagePart = ({ part, formatOptions, ...props }: { part: MessageFormatPart, formatOptions?: MessageFormatOptions } & ComponentProps<'span'>) => { - const { color, italic, bold, underlined, strikethrough, text, clickEvent, hoverEvent, obfuscated } = part + const { color: _color, italic, bold, underlined, strikethrough, text, clickEvent, hoverEvent, obfuscated } = part + const color = _color ?? 'white' const clickProps = clickEventToProps(clickEvent) const hoverMessageRaw = hoverItemToText(hoverEvent) @@ -77,7 +78,7 @@ export const MessagePart = ({ part, ...props }: { part: MessageFormatPart } & Co const applyStyles = [ clickProps && messageFormatStylesMap.clickEvent, - color ? colorF(color.toLowerCase()) + `; text-shadow: 1px 1px 0px ${getColorShadow(colorF(color.toLowerCase()).replace('color:', ''))}` : messageFormatStylesMap.white, + colorF(color.toLowerCase()) + ((formatOptions?.doShadow ?? true) ? `; text-shadow: 1px 1px 0px ${getColorShadow(colorF(color.toLowerCase()).replace('color:', ''))}` : ''), italic && messageFormatStylesMap.italic, bold && messageFormatStylesMap.bold, italic && messageFormatStylesMap.italic, @@ -89,10 +90,10 @@ export const MessagePart = ({ part, ...props }: { part: MessageFormatPart } & Co return <span title={hoverItemText} style={parseInlineStyle(applyStyles.join(';'))} {...clickProps} {...props}>{text}</span> } -export default ({ parts, className }: { parts: readonly MessageFormatPart[], className?: string }) => { +export default ({ parts, className, formatOptions }: { parts: readonly MessageFormatPart[], className?: string, formatOptions?: MessageFormatOptions }) => { return ( <span className={`formatted-message ${className ?? ''}`}> - {parts.map((part, i) => <MessagePart key={i} part={part} />)} + {parts.map((part, i) => <MessagePart key={i} part={part} formatOptions={formatOptions} />)} </span> ) } @@ -123,22 +124,22 @@ export function parseInlineStyle (style: string): Record<string, any> { } export const messageFormatStylesMap = { - black: 'color:#000000', - dark_blue: 'color:#0000AA', - dark_green: 'color:#00AA00', - dark_aqua: 'color:#00AAAA', - dark_red: 'color:#AA0000', - dark_purple: 'color:#AA00AA', - gold: 'color:#FFAA00', - gray: 'color:#AAAAAA', - dark_gray: 'color:#555555', - blue: 'color:#5555FF', - green: 'color:#55FF55', - aqua: 'color:#55FFFF', - red: 'color:#FF5555', - light_purple: 'color:#FF55FF', - yellow: 'color:#FFFF55', - white: 'color:#FFFFFF', + black: 'color:color(display-p3 0 0 0)', + dark_blue: 'color:color(display-p3 0 0 0.6667)', + dark_green: 'color:color(display-p3 0 0.6667 0)', + dark_aqua: 'color:color(display-p3 0 0.6667 0.6667)', + dark_red: 'color:color(display-p3 0.6667 0 0)', + dark_purple: 'color:color(display-p3 0.6667 0 0.6667)', + gold: 'color:color(display-p3 1 0.6667 0)', + gray: 'color:color(display-p3 0.6667 0.6667 0.6667)', + dark_gray: 'color:color(display-p3 0.3333 0.3333 0.3333)', + blue: 'color:color(display-p3 0.3333 0.3333 1)', + green: 'color:color(display-p3 0.3333 1 0.3333)', + aqua: 'color:color(display-p3 0.3333 1 1)', + red: 'color:color(display-p3 1 0.3333 0.3333)', + light_purple: 'color:color(display-p3 1 0.3333 1)', + yellow: 'color:color(display-p3 1 1 0.3333)', + white: 'color:color(display-p3 1 1 1)', bold: 'font-weight:900', strikethrough: 'text-decoration:line-through', underlined: 'text-decoration:underline', diff --git a/src/react/MessageFormattedString.tsx b/src/react/MessageFormattedString.tsx index f667cdd8..1fe8166a 100644 --- a/src/react/MessageFormattedString.tsx +++ b/src/react/MessageFormattedString.tsx @@ -1,14 +1,15 @@ import { useMemo } from 'react' import { fromFormattedString } from '@xmcl/text-component' import { ErrorBoundary } from '@zardoy/react-util' -import { formatMessage } from '../chatUtils' +import { formatMessage, MessageFormatOptions } from '../chatUtils' import MessageFormatted from './MessageFormatted' /** like MessageFormatted, but receives raw string or json instead, uses window.loadedData */ -export default ({ message, fallbackColor, className }: { +export default ({ message, fallbackColor, className, formatOptions }: { message: string | Record<string, any> | null, fallbackColor?: string, className?: string + formatOptions?: MessageFormatOptions }) => { const messageJson = useMemo(() => { if (!message) return null @@ -30,6 +31,6 @@ export default ({ message, fallbackColor, className }: { console.error(error) return <div>[text component crashed]</div> }}> - <MessageFormatted parts={messageJson} className={className} /> + <MessageFormatted parts={messageJson} className={className} formatOptions={formatOptions} /> </ErrorBoundary> : null } diff --git a/src/react/Scoreboard.css b/src/react/Scoreboard.css index b2bb8521..38ec426f 100644 --- a/src/react/Scoreboard.css +++ b/src/react/Scoreboard.css @@ -1,6 +1,6 @@ .scoreboard-container { z-index: -2; - pointer-events: none; + /* pointer-events: none; */ white-space: nowrap; position: fixed; right: 0px; From f3ff4bef0374bd0bdae44e3e5621cbf5996d1bd2 Mon Sep 17 00:00:00 2001 From: Vitaly <vital2580@icloud.com> Date: Wed, 18 Jun 2025 08:19:04 +0300 Subject: [PATCH 031/181] big renderer codebase cleanup: clean player state (#371) --- .cursor/rules/vars-usage.mdc | 16 ++ .gitignore | 1 + renderer/buildMesherWorker.mjs | 4 + renderer/viewer/baseGraphicsBackend.ts | 32 ++-- renderer/viewer/lib/basePlayerState.ts | 162 +++++++------------- renderer/viewer/lib/guiRenderer.ts | 26 ++-- renderer/viewer/lib/mesher/mesher.ts | 2 + renderer/viewer/lib/mesher/shared.ts | 2 +- renderer/viewer/lib/utils.ts | 5 +- renderer/viewer/lib/utils/skins.ts | 49 ++++-- renderer/viewer/lib/workerProxy.ts | 41 ++++- renderer/viewer/lib/worldDataEmitter.ts | 44 ++---- renderer/viewer/lib/worldrendererCommon.ts | 119 ++++++++------ renderer/viewer/three/appShared.ts | 18 +-- renderer/viewer/three/documentRenderer.ts | 155 ++++++++++++++----- renderer/viewer/three/entities.ts | 84 ++++++---- renderer/viewer/three/entity/EntityMesh.ts | 5 +- renderer/viewer/three/graphicsBackend.ts | 23 ++- renderer/viewer/three/holdingBlock.ts | 36 +++-- renderer/viewer/three/panorama.ts | 79 ++++++---- renderer/viewer/three/renderSlot.ts | 78 ++++++++++ renderer/viewer/three/world/cursorBlock.ts | 11 +- renderer/viewer/three/worldrendererThree.ts | 58 ++++--- src/appViewer.ts | 78 ++++++---- src/browserfs.ts | 6 +- src/controls.ts | 3 + src/index.ts | 44 ++++-- src/inventoryWindows.ts | 86 +---------- src/mineflayer/items.ts | 13 +- src/mineflayer/playerState.ts | 155 ++++++++----------- src/react/HotbarRenderApp.tsx | 2 +- src/react/Notification.tsx | 73 +++++++-- src/react/RendererDebugMenu.tsx | 6 +- src/react/Singleplayer.tsx | 2 +- src/rendererUtils.ts | 14 +- src/resourcePack.ts | 46 +++--- src/resourcesManager.ts | 105 +++++++------ 37 files changed, 975 insertions(+), 708 deletions(-) create mode 100644 .cursor/rules/vars-usage.mdc create mode 100644 renderer/viewer/three/renderSlot.ts diff --git a/.cursor/rules/vars-usage.mdc b/.cursor/rules/vars-usage.mdc new file mode 100644 index 00000000..7120e7ae --- /dev/null +++ b/.cursor/rules/vars-usage.mdc @@ -0,0 +1,16 @@ +--- +description: Restricts usage of the global Mineflayer `bot` variable to only src/ files; prohibits usage in renderer/. Specifies correct usage of player state and appViewer globals. +globs: src/**/*.ts,renderer/**/*.ts +alwaysApply: false +--- +Ask AI + +- The global variable `bot` refers to the Mineflayer bot instance. +- You may use `bot` directly in any file under the `src/` directory (e.g., `src/mineflayer/playerState.ts`). +- Do **not** use `bot` directly in any file under the `renderer/` directory or its subfolders (e.g., `renderer/viewer/three/worldrendererThree.ts`). +- In renderer code, all bot/player state and events must be accessed via explicit interfaces, state managers, or passed-in objects, never by referencing `bot` directly. +- In renderer code (such as in `WorldRendererThree`), use the `playerState` property (e.g., `worldRenderer.playerState.gameMode`) to access player state. The implementation for `playerState` lives in `src/mineflayer/playerState.ts`. +- In `src/` code, you may use the global variable `appViewer` from `src/appViewer.ts` directly. Do **not** import `appViewer` or use `window.appViewer`; use the global `appViewer` variable as-is. +- Some other global variables that can be used without window prefixes are listed in src/globals.d.ts + +Rationale: This ensures a clean separation between the Mineflayer logic (server-side/game logic) and the renderer (client-side/view logic), making the renderer portable and testable, and maintains proper usage of global state. diff --git a/.gitignore b/.gitignore index bd774315..33734572 100644 --- a/.gitignore +++ b/.gitignore @@ -19,5 +19,6 @@ generated storybook-static server-jar config.local.json +logs/ src/react/npmReactComponents.ts diff --git a/renderer/buildMesherWorker.mjs b/renderer/buildMesherWorker.mjs index 47ea5771..d88297a5 100644 --- a/renderer/buildMesherWorker.mjs +++ b/renderer/buildMesherWorker.mjs @@ -35,6 +35,10 @@ const buildOptions = { define: { 'process.env.BROWSER': '"true"', }, + loader: { + '.png': 'dataurl', + '.obj': 'text' + }, plugins: [ ...mesherSharedPlugins, { diff --git a/renderer/viewer/baseGraphicsBackend.ts b/renderer/viewer/baseGraphicsBackend.ts index 4724076a..3cd227de 100644 --- a/renderer/viewer/baseGraphicsBackend.ts +++ b/renderer/viewer/baseGraphicsBackend.ts @@ -1,16 +1,26 @@ -import { RendererReactiveState } from '../../src/appViewer' +import { NonReactiveState, RendererReactiveState } from '../../src/appViewer' -export const getDefaultRendererState = (): RendererReactiveState => { +export const getDefaultRendererState = (): { + reactive: RendererReactiveState + nonReactive: NonReactiveState +} => { return { - world: { - chunksLoaded: new Set(), - heightmaps: new Map(), - chunksTotalNumber: 0, - allChunksLoaded: true, - mesherWork: false, - intersectMedia: null + reactive: { + world: { + chunksLoaded: new Set(), + heightmaps: new Map(), + allChunksLoaded: true, + mesherWork: false, + intersectMedia: null + }, + renderer: '', + preventEscapeMenu: false }, - renderer: '', - preventEscapeMenu: false + nonReactive: { + world: { + chunksLoaded: new Set(), + chunksTotalNumber: 0, + } + } } } diff --git a/renderer/viewer/lib/basePlayerState.ts b/renderer/viewer/lib/basePlayerState.ts index 856772b1..fe6e39c4 100644 --- a/renderer/viewer/lib/basePlayerState.ts +++ b/renderer/viewer/lib/basePlayerState.ts @@ -1,125 +1,69 @@ -import { EventEmitter } from 'events' -import { Vec3 } from 'vec3' -import TypedEmitter from 'typed-emitter' import { ItemSelector } from 'mc-assets/dist/itemDefinitions' -import { proxy, ref } from 'valtio' import { GameMode } from 'mineflayer' -import { HandItemBlock } from '../three/holdingBlock' +import { proxy } from 'valtio' +import type { HandItemBlock } from '../three/holdingBlock' export type MovementState = 'NOT_MOVING' | 'WALKING' | 'SPRINTING' | 'SNEAKING' export type ItemSpecificContextProperties = Partial<Pick<ItemSelector['properties'], 'minecraft:using_item' | 'minecraft:use_duration' | 'minecraft:use_cycle' | 'minecraft:display_context'>> -export type PlayerStateEvents = { - heldItemChanged: (item: HandItemBlock | undefined, isLeftHand: boolean) => void -} - export type BlockShape = { position: any; width: any; height: any; depth: any; } export type BlocksShapes = BlockShape[] -export interface IPlayerState { - getEyeHeight(): number - getMovementState(): MovementState - getVelocity(): Vec3 - isOnGround(): boolean - isSneaking(): boolean - isFlying(): boolean - isSprinting (): boolean - getItemUsageTicks?(): number - getPosition(): Vec3 - // isUsingItem?(): boolean - getHeldItem?(isLeftHand: boolean): HandItemBlock | undefined - username?: string - onlineMode?: boolean - lightingDisabled?: boolean - shouldHideHand?: boolean +// edit src/mineflayer/playerState.ts for implementation of player state from mineflayer +export const getInitialPlayerState = () => proxy({ + playerSkin: undefined as string | undefined, + inWater: false, + waterBreathing: false, + backgroundColor: [0, 0, 0] as [number, number, number], + ambientLight: 0, + directionalLight: 0, + eyeHeight: 0, + gameMode: undefined as GameMode | undefined, + lookingAtBlock: undefined as { + x: number + y: number + z: number + face?: number + shapes: BlocksShapes + } | undefined, + diggingBlock: undefined as { + x: number + y: number + z: number + stage: number + face?: number + mergedShape: BlockShape | undefined + } | undefined, + movementState: 'NOT_MOVING' as MovementState, + onGround: true, + sneaking: false, + flying: false, + sprinting: false, + itemUsageTicks: 0, + username: '', + onlineMode: false, + lightingDisabled: false, + shouldHideHand: false, + heldItemMain: undefined as HandItemBlock | undefined, + heldItemOff: undefined as HandItemBlock | undefined, +}) - events: TypedEmitter<PlayerStateEvents> +export const getInitialPlayerStateRenderer = () => ({ + reactive: getInitialPlayerState() +}) - reactive: { - playerSkin: string | undefined - inWater: boolean - waterBreathing: boolean - backgroundColor: [number, number, number] - ambientLight: number - directionalLight: number - gameMode?: GameMode - lookingAtBlock?: { - x: number - y: number - z: number - face?: number - shapes: BlocksShapes - } - diggingBlock?: { - x: number - y: number - z: number - stage: number - face?: number - mergedShape?: BlockShape - } - } +export type PlayerStateReactive = ReturnType<typeof getInitialPlayerState> + +export interface PlayerStateRenderer { + reactive: PlayerStateReactive } -export class BasePlayerState implements IPlayerState { - reactive = proxy({ - playerSkin: undefined as string | undefined, - inWater: false, - waterBreathing: false, - backgroundColor: ref([0, 0, 0]) as [number, number, number], - ambientLight: 0, - directionalLight: 0, - }) - protected movementState: MovementState = 'NOT_MOVING' - protected velocity = new Vec3(0, 0, 0) - protected onGround = true - protected sneaking = false - protected flying = false - protected sprinting = false - readonly events = new EventEmitter() as TypedEmitter<PlayerStateEvents> - - getEyeHeight (): number { - return 1.62 - } - - getMovementState (): MovementState { - return this.movementState - } - - getVelocity (): Vec3 { - return this.velocity - } - - isOnGround (): boolean { - return this.onGround - } - - isSneaking (): boolean { - return this.sneaking - } - - isFlying (): boolean { - return this.flying - } - - isSprinting (): boolean { - return this.sprinting - } - - getPosition (): Vec3 { - return new Vec3(0, 0, 0) - } - - // For testing purposes - setState (state: Partial<{ - movementState: MovementState - velocity: Vec3 - onGround: boolean - sneaking: boolean - flying: boolean - sprinting: boolean - }>) { - Object.assign(this, state) +export const getItemSelector = (playerState: PlayerStateRenderer, specificProperties: ItemSpecificContextProperties, item?: import('prismarine-item').Item) => { + return { + ...specificProperties, + 'minecraft:date': new Date(), + // "minecraft:context_dimension": bot.entityp, + // 'minecraft:time': bot.time.timeOfDay / 24_000, } } diff --git a/renderer/viewer/lib/guiRenderer.ts b/renderer/viewer/lib/guiRenderer.ts index 2689126e..709941dc 100644 --- a/renderer/viewer/lib/guiRenderer.ts +++ b/renderer/viewer/lib/guiRenderer.ts @@ -9,11 +9,6 @@ import { makeTextureAtlas } from 'mc-assets/dist/atlasCreator' import { proxy, ref } from 'valtio' import { getItemDefinition } from 'mc-assets/dist/itemDefinitions' -export const activeGuiAtlas = proxy({ - atlas: null as null | { json, image }, - version: 0 -}) - export const getNonFullBlocksModels = () => { let version = appViewer.resourcesManager.currentResources!.version ?? 'latest' if (versionToNumber(version) < versionToNumber('1.13')) version = '1.13' @@ -122,18 +117,18 @@ const RENDER_SIZE = 64 const generateItemsGui = async (models: Record<string, BlockModelMcAssets>, isItems = false) => { const { currentResources } = appViewer.resourcesManager - const img = await getLoadedImage(isItems ? currentResources!.itemsAtlasParser.latestImage : currentResources!.blocksAtlasParser.latestImage) + const imgBitmap = isItems ? currentResources!.itemsAtlasImage : currentResources!.blocksAtlasImage const canvasTemp = document.createElement('canvas') - canvasTemp.width = img.width - canvasTemp.height = img.height + canvasTemp.width = imgBitmap.width + canvasTemp.height = imgBitmap.height canvasTemp.style.imageRendering = 'pixelated' const ctx = canvasTemp.getContext('2d')! ctx.imageSmoothingEnabled = false - ctx.drawImage(img, 0, 0) + ctx.drawImage(imgBitmap, 0, 0) - const atlasParser = isItems ? currentResources!.itemsAtlasParser : currentResources!.blocksAtlasParser + const atlasParser = isItems ? appViewer.resourcesManager.itemsAtlasParser : appViewer.resourcesManager.blocksAtlasParser const textureAtlas = new TextureAtlas( - ctx.getImageData(0, 0, img.width, img.height), + ctx.getImageData(0, 0, imgBitmap.width, imgBitmap.height), Object.fromEntries(Object.entries(atlasParser.atlas.latest.textures).map(([key, value]) => { return [key, [ value.u, @@ -243,6 +238,9 @@ const generateItemsGui = async (models: Record<string, BlockModelMcAssets>, isIt return images } +/** + * @mainThread + */ const generateAtlas = async (images: Record<string, HTMLImageElement>) => { const atlas = makeTextureAtlas({ input: Object.keys(images), @@ -260,9 +258,9 @@ const generateAtlas = async (images: Record<string, HTMLImageElement>) => { // a.download = 'blocks_atlas.png' // a.click() - activeGuiAtlas.atlas = { + appViewer.resourcesManager.currentResources!.guiAtlas = { json: atlas.json, - image: ref(await getLoadedImage(atlas.canvas.toDataURL())), + image: await createImageBitmap(atlas.canvas), } return atlas @@ -279,6 +277,6 @@ export const generateGuiAtlas = async () => { const itemImages = await generateItemsGui(itemsModelsResolved, true) console.timeEnd('generate items gui atlas') await generateAtlas({ ...blockImages, ...itemImages }) - activeGuiAtlas.version++ + appViewer.resourcesManager.currentResources!.guiAtlasVersion++ // await generateAtlas(blockImages) } diff --git a/renderer/viewer/lib/mesher/mesher.ts b/renderer/viewer/lib/mesher/mesher.ts index f26d8022..a063d77f 100644 --- a/renderer/viewer/lib/mesher/mesher.ts +++ b/renderer/viewer/lib/mesher/mesher.ts @@ -77,6 +77,7 @@ const handleMessage = data => { if (data.type === 'mcData') { globalVar.mcData = data.mcData + globalVar.loadedData = data.mcData } if (data.config) { @@ -138,6 +139,7 @@ const handleMessage = data => { dirtySections = new Map() // todo also remove cached globalVar.mcData = null + globalVar.loadedData = null allDataReady = false break diff --git a/renderer/viewer/lib/mesher/shared.ts b/renderer/viewer/lib/mesher/shared.ts index 82416fab..53e0c534 100644 --- a/renderer/viewer/lib/mesher/shared.ts +++ b/renderer/viewer/lib/mesher/shared.ts @@ -9,7 +9,7 @@ export const defaultMesherConfig = { skyLight: 15, smoothLighting: true, outputFormat: 'threeJs' as 'threeJs' | 'webgpu', - textureSize: 1024, // for testing + // textureSize: 1024, // for testing debugModelVariant: undefined as undefined | number[], clipWorldBelowY: undefined as undefined | number, disableSignsMapsSupport: false diff --git a/renderer/viewer/lib/utils.ts b/renderer/viewer/lib/utils.ts index a1574b5c..5c9c00f1 100644 --- a/renderer/viewer/lib/utils.ts +++ b/renderer/viewer/lib/utils.ts @@ -1,4 +1,5 @@ import * as THREE from 'three' +import { loadThreeJsTextureFromUrl, loadThreeJsTextureFromUrlSync } from './utils/skins' let textureCache: Record<string, THREE.Texture> = {} let imagesPromises: Record<string, Promise<THREE.Texture>> = {} @@ -7,7 +8,9 @@ export async function loadTexture (texture: string, cb: (texture: THREE.Texture) const cached = textureCache[texture] if (!cached) { const { promise, resolve } = Promise.withResolvers<THREE.Texture>() - textureCache[texture] = new THREE.TextureLoader().load(texture, resolve) + const t = loadThreeJsTextureFromUrlSync(texture) + textureCache[texture] = t.texture + void t.promise.then(resolve) imagesPromises[texture] = promise } diff --git a/renderer/viewer/lib/utils/skins.ts b/renderer/viewer/lib/utils/skins.ts index 98792820..e0c8c32e 100644 --- a/renderer/viewer/lib/utils/skins.ts +++ b/renderer/viewer/lib/utils/skins.ts @@ -1,18 +1,43 @@ import { loadSkinToCanvas } from 'skinview-utils' import * as THREE from 'three' import stevePng from 'mc-assets/dist/other-textures/latest/entity/player/wide/steve.png' +import { getLoadedImage } from 'mc-assets/dist/utils' -// eslint-disable-next-line unicorn/prefer-export-from -export const stevePngUrl = stevePng -export const steveTexture = new THREE.TextureLoader().loadAsync(stevePng) - -export async function loadImageFromUrl (imageUrl: string): Promise<HTMLImageElement> { - const img = new Image() - img.src = imageUrl - await new Promise<void>(resolve => { - img.onload = () => resolve() +export const loadThreeJsTextureFromUrlSync = (imageUrl: string) => { + const texture = new THREE.Texture() + const promise = getLoadedImage(imageUrl).then(image => { + texture.image = image + texture.needsUpdate = true + return texture }) - return img + return { + texture, + promise + } +} + +export const loadThreeJsTextureFromUrl = async (imageUrl: string) => { + const loaded = new THREE.TextureLoader().loadAsync(imageUrl) + return loaded +} +export const loadThreeJsTextureFromBitmap = (image: ImageBitmap) => { + const canvas = new OffscreenCanvas(image.width, image.height) + const ctx = canvas.getContext('2d')! + ctx.drawImage(image, 0, 0) + const texture = new THREE.Texture(canvas) + texture.magFilter = THREE.NearestFilter + texture.minFilter = THREE.NearestFilter + return texture +} + +export const stevePngUrl = stevePng +export const steveTexture = loadThreeJsTextureFromUrl(stevePngUrl) + + +export async function loadImageFromUrl (imageUrl: string): Promise<ImageBitmap> { + const response = await fetch(imageUrl) + const blob = await response.blob() + return createImageBitmap(blob) } const config = { @@ -52,13 +77,13 @@ export const parseSkinTexturesValue = (value: string) => { return decodedData.textures?.SKIN?.url } -export async function loadSkinImage (skinUrl: string): Promise<{ canvas: HTMLCanvasElement, image: HTMLImageElement }> { +export async function loadSkinImage (skinUrl: string): Promise<{ canvas: OffscreenCanvas, image: ImageBitmap }> { if (!skinUrl.startsWith('data:')) { skinUrl = await fetchAndConvertBase64Skin(skinUrl.replace('http://', 'https://')) } const image = await loadImageFromUrl(skinUrl) - const skinCanvas = document.createElement('canvas') + const skinCanvas = new OffscreenCanvas(64, 64) loadSkinToCanvas(skinCanvas, image) return { canvas: skinCanvas, image } } diff --git a/renderer/viewer/lib/workerProxy.ts b/renderer/viewer/lib/workerProxy.ts index 9d8e7fcc..2b38dca9 100644 --- a/renderer/viewer/lib/workerProxy.ts +++ b/renderer/viewer/lib/workerProxy.ts @@ -1,9 +1,20 @@ -export function createWorkerProxy<T extends Record<string, (...args: any[]) => void>> (handlers: T, channel?: MessagePort): { __workerProxy: T } { +import { proxy, getVersion, subscribe } from 'valtio' + +export function createWorkerProxy<T extends Record<string, (...args: any[]) => void | Promise<any>>> (handlers: T, channel?: MessagePort): { __workerProxy: T } { const target = channel ?? globalThis target.addEventListener('message', (event: any) => { - const { type, args } = event.data + const { type, args, msgId } = event.data if (handlers[type]) { - handlers[type](...args) + const result = handlers[type](...args) + if (result instanceof Promise) { + void result.then((result) => { + target.postMessage({ + type: 'result', + msgId, + args: [result] + }) + }) + } } }) return null as any @@ -23,6 +34,7 @@ export function createWorkerProxy<T extends Record<string, (...args: any[]) => v export const useWorkerProxy = <T extends { __workerProxy: Record<string, (...args: any[]) => void> }> (worker: Worker | MessagePort, autoTransfer = true): T['__workerProxy'] & { transfer: (...args: Transferable[]) => T['__workerProxy'] } => { + let messageId = 0 // in main thread return new Proxy({} as any, { get (target, prop) { @@ -41,11 +53,30 @@ export const useWorkerProxy = <T extends { __workerProxy: Record<string, (...arg } } return (...args: any[]) => { - const transfer = autoTransfer ? args.filter(arg => arg instanceof ArrayBuffer || arg instanceof MessagePort || arg instanceof ImageBitmap || arg instanceof OffscreenCanvas || arg instanceof ImageData) : [] + const msgId = messageId++ + const transfer = autoTransfer ? args.filter(arg => { + return arg instanceof ArrayBuffer || arg instanceof MessagePort + || (typeof ImageBitmap !== 'undefined' && arg instanceof ImageBitmap) + || (typeof OffscreenCanvas !== 'undefined' && arg instanceof OffscreenCanvas) + || (typeof ImageData !== 'undefined' && arg instanceof ImageData) + }) : [] worker.postMessage({ type: prop, + msgId, args, - }, transfer as any[]) + }, transfer) + return { + // eslint-disable-next-line unicorn/no-thenable + then (onfulfilled: (value: any) => void) { + const handler = ({ data }: MessageEvent): void => { + if (data.type === 'result' && data.msgId === msgId) { + onfulfilled(data.args[0]) + worker.removeEventListener('message', handler as EventListener) + } + } + worker.addEventListener('message', handler as EventListener) + } + } } } }) diff --git a/renderer/viewer/lib/worldDataEmitter.ts b/renderer/viewer/lib/worldDataEmitter.ts index 9e483ee2..86f372d1 100644 --- a/renderer/viewer/lib/worldDataEmitter.ts +++ b/renderer/viewer/lib/worldDataEmitter.ts @@ -7,9 +7,7 @@ import { Vec3 } from 'vec3' import { BotEvents } from 'mineflayer' import { proxy } from 'valtio' import TypedEmitter from 'typed-emitter' -import { getItemFromBlock } from '../../../src/chatUtils' import { delayedIterator } from '../../playground/shared' -import { playerState } from '../../../src/mineflayer/playerState' import { chunkPos } from './simpleUtils' export type ChunkPosKey = string // like '16,16' @@ -23,7 +21,6 @@ export type WorldDataEmitterEvents = { time: (data: number) => void renderDistance: (viewDistance: number) => void blockEntities: (data: Record<string, any> | { blockEntities: Record<string, any> }) => void - listening: () => void markAsLoaded: (data: { x: number, z: number }) => void unloadChunk: (data: { x: number, z: number }) => void loadChunk: (data: { x: number, z: number, chunk: string, blockEntities: any, worldConfig: any, isLightUpdate: boolean }) => void @@ -32,10 +29,10 @@ export type WorldDataEmitterEvents = { end: () => void } -/** - * Usually connects to mineflayer bot and emits world data (chunks, entities) - * It's up to the consumer to serialize the data if needed - */ +export class WorldDataEmitterWorker extends (EventEmitter as new () => TypedEmitter<WorldDataEmitterEvents>) { + static readonly restorerName = 'WorldDataEmitterWorker' +} + export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<WorldDataEmitterEvents>) { loadedChunks: Record<ChunkPosKey, boolean> readonly lastPos: Vec3 @@ -57,11 +54,6 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo /* config */ isPlayground = false /* config */ allowPositionUpdate = true - public reactive = proxy({ - cursorBlock: null as Vec3 | null, - cursorBlockBreakingStage: null as number | null, - }) - constructor (public world: typeof __type_bot['world'], public viewDistance: number, position: Vec3 = new Vec3(0, 0, 0)) { // eslint-disable-next-line constructor-super super() @@ -171,22 +163,6 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo } }) - this.emitter.on('listening', () => { - this.emitter.emit('blockEntities', new Proxy({}, { - get (_target, posKey, receiver) { - if (typeof posKey !== 'string') return - const [x, y, z] = posKey.split(',').map(Number) - return bot.world.getBlock(new Vec3(x, y, z))?.entity - }, - })) - this.emitter.emit('renderDistance', this.viewDistance) - this.emitter.emit('time', bot.time.timeOfDay) - }) - // node.js stream data event pattern - if (this.emitter.listenerCount('blockEntities')) { - this.emitter.emit('listening') - } - for (const [evt, listener] of Object.entries(this.eventListeners)) { bot.on(evt as any, listener) } @@ -200,8 +176,16 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo console.error('error processing entity', err) } } + } - void this.init(bot.entity.position) + emitterGotConnected () { + this.emitter.emit('blockEntities', new Proxy({}, { + get (_target, posKey, receiver) { + if (typeof posKey !== 'string') return + const [x, y, z] = posKey.split(',').map(Number) + return bot.world.getBlock(new Vec3(x, y, z))?.entity + }, + })) } removeListenersFromBot (bot: import('mineflayer').Bot) { @@ -213,6 +197,8 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo async init (pos: Vec3) { this.updateViewDistance(this.viewDistance) this.emitter.emit('chunkPosUpdate', { pos }) + this.emitter.emit('time', bot.time.timeOfDay) + this.emitterGotConnected() const [botX, botZ] = chunkPos(pos) const positions = generateSpiralMatrix(this.viewDistance).map(([x, z]) => new Vec3((botX + x) * 16, 0, (botZ + z) * 16)) diff --git a/renderer/viewer/lib/worldrendererCommon.ts b/renderer/viewer/lib/worldrendererCommon.ts index 2b37742c..1f0df0bd 100644 --- a/renderer/viewer/lib/worldrendererCommon.ts +++ b/renderer/viewer/lib/worldrendererCommon.ts @@ -1,25 +1,22 @@ /* eslint-disable guard-for-in */ import { EventEmitter } from 'events' import { Vec3 } from 'vec3' -import * as THREE from 'three' import mcDataRaw from 'minecraft-data/data.js' // note: using alias import TypedEmitter from 'typed-emitter' -import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer' import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider' import { generateSpiralMatrix } from 'flying-squid/dist/utils' import { subscribeKey } from 'valtio/utils' import { proxy } from 'valtio' import { dynamicMcDataFiles } from '../../buildMesherConfig.mjs' -import { toMajorVersion } from '../../../src/utils' -import { ResourcesManager } from '../../../src/resourcesManager' +import type { ResourcesManagerTransferred } from '../../../src/resourcesManager' import { DisplayWorldOptions, GraphicsInitOptions, RendererReactiveState } from '../../../src/appViewer' import { SoundSystem } from '../three/threeJsSound' import { buildCleanupDecorator } from './cleanupDecorator' -import { HighestBlockInfo, MesherGeometryOutput, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig, MesherMainEvent } from './mesher/shared' +import { HighestBlockInfo, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig, MesherMainEvent } from './mesher/shared' import { chunkPos } from './simpleUtils' -import { addNewStat, removeAllStats, removeStat, updatePanesVisibility, updateStatText } from './ui/newStats' -import { WorldDataEmitter } from './worldDataEmitter' -import { IPlayerState } from './basePlayerState' +import { addNewStat, removeAllStats, updatePanesVisibility, updateStatText } from './ui/newStats' +import { WorldDataEmitterWorker } from './worldDataEmitter' +import { PlayerStateRenderer } from './basePlayerState' import { MesherLogReader } from './mesherlogReader' import { setSkinsConfig } from './utils/skins' @@ -27,6 +24,11 @@ function mod (x, n) { return ((x % n) + n) % n } +const toMajorVersion = version => { + const [a, b] = (String(version)).split('.') + return `${a}.${b}` +} + export const worldCleanup = buildCleanupDecorator('resetWorld') export const defaultWorldRendererConfig = { @@ -52,7 +54,8 @@ export const defaultWorldRendererConfig = { foreground: true, enableDebugOverlay: false, _experimentalSmoothChunkLoading: true, - _renderByChunks: false + _renderByChunks: false, + volume: 1 } export type WorldRendererConfig = typeof defaultWorldRendererConfig @@ -153,7 +156,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> abstract changeBackgroundColor (color: [number, number, number]): void worldRendererConfig: WorldRendererConfig - playerState: IPlayerState + playerState: PlayerStateRenderer reactiveState: RendererReactiveState mesherLogReader: MesherLogReader | undefined forceCallFromMesherReplayer = false @@ -169,6 +172,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> } currentRenderedFrames = 0 fpsAverage = 0 + lastFps = 0 fpsWorst = undefined as number | undefined fpsSamples = 0 mainThreadRendering = true @@ -184,7 +188,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> return (this.initOptions.config.statsVisible ?? 0) > 1 } - constructor (public readonly resourcesManager: ResourcesManager, public displayOptions: DisplayWorldOptions, public initOptions: GraphicsInitOptions) { + constructor (public readonly resourcesManager: ResourcesManagerTransferred, public displayOptions: DisplayWorldOptions, public initOptions: GraphicsInitOptions) { this.snapshotInitialValues() this.worldRendererConfig = displayOptions.inWorldRenderingConfig this.playerState = displayOptions.playerState @@ -221,6 +225,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> } else { this.fpsWorst = Math.min(this.fpsWorst, this.currentRenderedFrames) } + this.lastFps = this.currentRenderedFrames this.currentRenderedFrames = 0 } @@ -231,15 +236,11 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> async init () { if (this.active) throw new Error('WorldRendererCommon is already initialized') - await this.resourcesManager.loadMcData(this.version) - if (!this.resourcesManager.currentResources) { - await this.resourcesManager.updateAssetsData({ }) - } await Promise.all([ this.resetWorkers(), (async () => { - if (this.resourcesManager.currentResources) { + if (this.resourcesManager.currentResources?.allReady) { await this.updateAssetsData() } })() @@ -291,35 +292,22 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> initWorkers (numWorkers = this.worldRendererConfig.mesherWorkers) { // init workers for (let i = 0; i < numWorkers + 1; i++) { - // Node environment needs an absolute path, but browser needs the url of the file - const workerName = 'mesher.js' - // eslint-disable-next-line node/no-path-concat - const src = typeof window === 'undefined' ? `${__dirname}/${workerName}` : workerName - - let worker: any - if (process.env.SINGLE_FILE_BUILD) { - const workerCode = document.getElementById('mesher-worker-code')!.textContent! - const blob = new Blob([workerCode], { type: 'text/javascript' }) - worker = new Worker(window.URL.createObjectURL(blob)) - } else { - worker = new Worker(src) - } - - worker.onmessage = ({ data }) => { + const worker = initMesherWorker((data) => { if (Array.isArray(data)) { this.messageQueue.push(...data) } else { this.messageQueue.push(data) } void this.processMessageQueue('worker') - } - if (worker.on) worker.on('message', (data) => { worker.onmessage({ data }) }) + }) this.workers.push(worker) } } - onReactiveValueUpdated<T extends keyof typeof this.displayOptions.playerState.reactive>(key: T, callback: (value: typeof this.displayOptions.playerState.reactive[T]) => void) { - callback(this.displayOptions.playerState.reactive[key]) + onReactivePlayerStateUpdated<T extends keyof typeof this.displayOptions.playerState.reactive>(key: T, callback: (value: typeof this.displayOptions.playerState.reactive[T]) => void, initial = true) { + if (initial) { + callback(this.displayOptions.playerState.reactive[key]) + } subscribeKey(this.displayOptions.playerState.reactive, key, callback) } @@ -334,7 +322,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> } watchReactivePlayerState () { - this.onReactiveValueUpdated('backgroundColor', (value) => { + this.onReactivePlayerStateUpdated('backgroundColor', (value) => { this.changeBackgroundColor(value) }) } @@ -466,7 +454,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> } if (data.type === 'heightmap') { - appViewer.rendererState.world.heightmaps.set(data.key, new Uint8Array(data.heightmap)) + this.reactiveState.world.heightmaps.set(data.key, new Uint8Array(data.heightmap)) } } @@ -543,7 +531,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> this.resetWorld() // for workers in single file build - if (document?.readyState === 'loading') { + if (typeof document !== 'undefined' && document?.readyState === 'loading') { await new Promise(resolve => { document.addEventListener('DOMContentLoaded', resolve) }) @@ -575,7 +563,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> skyLight, smoothLighting: this.worldRendererConfig.smoothLighting, outputFormat: this.outputFormat, - textureSize: this.resourcesManager.currentResources!.blocksAtlasParser.atlas.latest.width, + // textureSize: this.resourcesManager.currentResources!.blocksAtlasParser.atlas.latest.width, debugModelVariant: undefined, clipWorldBelowY: this.worldRendererConfig.clipWorldBelowY, disableSignsMapsSupport: !this.worldRendererConfig.extraBlockRenderers, @@ -600,7 +588,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> } async updateAssetsData () { - const resources = this.resourcesManager.currentResources! + const resources = this.resourcesManager.currentResources if (this.workers.length === 0) throw new Error('workers not initialized yet') for (const [i, worker] of this.workers.entries()) { @@ -610,7 +598,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> type: 'mesherData', workerIndex: i, blocksAtlas: { - latest: resources.blocksAtlasParser.atlas.latest + latest: resources.blocksAtlasJson }, blockstatesModels, config: this.getMesherConfig(), @@ -733,7 +721,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> lightUpdate (chunkX: number, chunkZ: number) { } - connect (worldView: WorldDataEmitter) { + connect (worldView: WorldDataEmitterWorker) { const worldEmitter = worldView worldEmitter.on('entity', (e) => { @@ -812,7 +800,16 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> }) worldEmitter.on('onWorldSwitch', () => { - for (const fn of this.onWorldSwitched) fn() + for (const fn of this.onWorldSwitched) { + try { + fn() + } catch (e) { + setTimeout(() => { + console.log('[Renderer Backend] Error in onWorldSwitched:') + throw e + }, 0) + } + } }) worldEmitter.on('time', (timeOfDay) => { @@ -830,8 +827,6 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> // (this).rerenderAllChunks?.() // } }) - - worldEmitter.emit('listening') } setBlockStateIdInner (pos: Vec3, stateId: number | undefined, needAoRecalculation = true) { @@ -1029,3 +1024,37 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> removeAllStats() } } + +export const initMesherWorker = (onGotMessage: (data: any) => void) => { + // Node environment needs an absolute path, but browser needs the url of the file + const workerName = 'mesher.js' + + let worker: any + if (process.env.SINGLE_FILE_BUILD) { + const workerCode = document.getElementById('mesher-worker-code')!.textContent! + const blob = new Blob([workerCode], { type: 'text/javascript' }) + worker = new Worker(window.URL.createObjectURL(blob)) + } else { + worker = new Worker(workerName) + } + + worker.onmessage = ({ data }) => { + onGotMessage(data) + } + if (worker.on) worker.on('message', (data) => { worker.onmessage({ data }) }) + return worker +} + +export const meshersSendMcData = (workers: Worker[], version: string, addData = {} as Record<string, any>) => { + const allMcData = mcDataRaw.pc[version] ?? mcDataRaw.pc[toMajorVersion(version)] + const mcData = { + version: JSON.parse(JSON.stringify(allMcData.version)) + } + for (const key of dynamicMcDataFiles) { + mcData[key] = allMcData[key] + } + + for (const worker of workers) { + worker.postMessage({ type: 'mcData', mcData, ...addData }) + } +} diff --git a/renderer/viewer/three/appShared.ts b/renderer/viewer/three/appShared.ts index 1dfb343c..5be9e10b 100644 --- a/renderer/viewer/three/appShared.ts +++ b/renderer/viewer/three/appShared.ts @@ -1,16 +1,16 @@ import { BlockModel } from 'mc-assets/dist/types' -import { ItemSpecificContextProperties } from 'renderer/viewer/lib/basePlayerState' -import { renderSlot } from '../../../src/inventoryWindows' +import { ItemSpecificContextProperties, PlayerStateRenderer } from 'renderer/viewer/lib/basePlayerState' import { GeneralInputItem, getItemModelName } from '../../../src/mineflayer/items' -import { ResourcesManager } from '../../../src/resourcesManager' +import { ResourcesManager, ResourcesManagerTransferred } from '../../../src/resourcesManager' +import { renderSlot } from './renderSlot' -export const getItemUv = (item: Record<string, any>, specificProps: ItemSpecificContextProperties, resourcesManager: ResourcesManager): { +export const getItemUv = (item: Record<string, any>, specificProps: ItemSpecificContextProperties, resourcesManager: ResourcesManagerTransferred, playerState: PlayerStateRenderer): { u: number v: number su: number sv: number renderInfo?: ReturnType<typeof renderSlot> - texture: HTMLImageElement + // texture: ImageBitmap modelName: string } | { resolvedModel: BlockModel @@ -30,11 +30,11 @@ export const getItemUv = (item: Record<string, any>, specificProps: ItemSpecific const model = getItemModelName({ ...item, name, - } as GeneralInputItem, specificProps, resourcesManager) + } as GeneralInputItem, specificProps, resourcesManager, playerState) const renderInfo = renderSlot({ modelName: model, - }, false, true) + }, resourcesManager, false, true) if (!renderInfo) throw new Error(`Failed to get render info for item ${name}`) @@ -53,7 +53,7 @@ export const getItemUv = (item: Record<string, any>, specificProps: ItemSpecific return { u, v, su, sv, renderInfo, - texture: img, + // texture: img, modelName: renderInfo.modelName! } } @@ -67,7 +67,7 @@ export const getItemUv = (item: Record<string, any>, specificProps: ItemSpecific v: 0, su: 16 / resources.blocksAtlasImage.width, sv: 16 / resources.blocksAtlasImage.width, - texture: resources.blocksAtlasImage, + // texture: resources.blocksAtlasImage, modelName: 'missing' } } diff --git a/renderer/viewer/three/documentRenderer.ts b/renderer/viewer/three/documentRenderer.ts index 1d556c2a..8ff31e69 100644 --- a/renderer/viewer/three/documentRenderer.ts +++ b/renderer/viewer/three/documentRenderer.ts @@ -6,15 +6,20 @@ import { GraphicsBackendConfig, GraphicsInitOptions } from '../../../src/appView import { WorldRendererConfig } from '../lib/worldrendererCommon' export class DocumentRenderer { - readonly canvas = document.createElement('canvas') + canvas: HTMLCanvasElement | OffscreenCanvas readonly renderer: THREE.WebGLRenderer private animationFrameId?: number + private timeoutId?: number private lastRenderTime = 0 - private previousWindowWidth = window.innerWidth - private previousWindowHeight = window.innerHeight + + private previousCanvasWidth = 0 + private previousCanvasHeight = 0 + private currentWidth = 0 + private currentHeight = 0 + private renderedFps = 0 private fpsInterval: any - private readonly stats: TopRightStats + private readonly stats: TopRightStats | undefined private paused = false disconnected = false preRender = () => { } @@ -26,9 +31,16 @@ export class DocumentRenderer { onRender = [] as Array<(sizeChanged: boolean) => void> inWorldRenderingConfig: WorldRendererConfig | undefined - constructor (initOptions: GraphicsInitOptions) { + constructor (initOptions: GraphicsInitOptions, public externalCanvas?: OffscreenCanvas) { this.config = initOptions.config + // Handle canvas creation/transfer based on context + if (externalCanvas) { + this.canvas = externalCanvas + } else { + this.addToPage() + } + try { this.renderer = new THREE.WebGLRenderer({ canvas: this.canvas, @@ -37,17 +49,24 @@ export class DocumentRenderer { powerPreference: this.config.powerPreference }) } catch (err) { - initOptions.displayCriticalError(new Error(`Failed to create WebGL context, not possible to render (restart browser): ${err.message}`)) + initOptions.callbacks.displayCriticalError(new Error(`Failed to create WebGL context, not possible to render (restart browser): ${err.message}`)) throw err } this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace - this.updatePixelRatio() - this.updateSize() - this.addToPage() + if (!externalCanvas) { + this.updatePixelRatio() + } + this.sizeUpdated() + // Initialize previous dimensions + this.previousCanvasWidth = this.canvas.width + this.previousCanvasHeight = this.canvas.height - this.stats = new TopRightStats(this.canvas, this.config.statsVisible) + // Only initialize stats and DOM-related features in main thread + if (!externalCanvas) { + this.stats = new TopRightStats(this.canvas as HTMLCanvasElement, this.config.statsVisible) + this.setupFpsTracking() + } - this.setupFpsTracking() this.startRenderLoop() } @@ -59,15 +78,33 @@ export class DocumentRenderer { this.renderer.setPixelRatio(pixelRatio) } - updateSize () { - this.renderer.setSize(window.innerWidth, window.innerHeight) + sizeUpdated () { + this.renderer.setSize(this.currentWidth, this.currentHeight, false) } private addToPage () { - this.canvas.id = 'viewer-canvas' - this.canvas.style.width = '100%' - this.canvas.style.height = '100%' - document.body.appendChild(this.canvas) + this.canvas = addCanvasToPage() + this.updateCanvasSize() + } + + updateSizeExternal (newWidth: number, newHeight: number, pixelRatio: number) { + this.currentWidth = newWidth + this.currentHeight = newHeight + this.renderer.setPixelRatio(pixelRatio) + this.sizeUpdated() + } + + private updateCanvasSize () { + if (!this.externalCanvas) { + const innnerWidth = window.innerWidth + const innnerHeight = window.innerHeight + if (this.currentWidth !== innnerWidth) { + this.currentWidth = innnerWidth + } + if (this.currentHeight !== innnerHeight) { + this.currentHeight = innnerHeight + } + } } private setupFpsTracking () { @@ -81,20 +118,15 @@ export class DocumentRenderer { }, 1000) } - // private handleResize () { - // const width = window.innerWidth - // const height = window.innerHeight - - // viewer.camera.aspect = width / height - // viewer.camera.updateProjectionMatrix() - // this.renderer.setSize(width, height) - // viewer.world.handleResize() - // } - private startRenderLoop () { const animate = () => { if (this.disconnected) return - this.animationFrameId = requestAnimationFrame(animate) + + if (this.config.timeoutRendering) { + this.timeoutId = setTimeout(animate, this.config.fpsLimit ? 1000 / this.config.fpsLimit : 0) as unknown as number + } else { + this.animationFrameId = requestAnimationFrame(animate) + } if (this.paused || (this.renderer.xr.isPresenting && !this.inWorldRenderingConfig?.vrPageGameRendering)) return @@ -112,18 +144,19 @@ export class DocumentRenderer { } let sizeChanged = false - if (this.previousWindowWidth !== window.innerWidth || this.previousWindowHeight !== window.innerHeight) { - this.previousWindowWidth = window.innerWidth - this.previousWindowHeight = window.innerHeight - this.updateSize() + this.updateCanvasSize() + if (this.previousCanvasWidth !== this.currentWidth || this.previousCanvasHeight !== this.currentHeight) { + this.previousCanvasWidth = this.currentWidth + this.previousCanvasHeight = this.currentHeight + this.sizeUpdated() sizeChanged = true } this.frameRender(sizeChanged) - // Update stats visibility each frame + // Update stats visibility each frame (main thread only) if (this.config.statsVisible !== undefined) { - this.stats.setVisibility(this.config.statsVisible) + this.stats?.setVisibility(this.config.statsVisible) } } @@ -132,16 +165,16 @@ export class DocumentRenderer { frameRender (sizeChanged: boolean) { this.preRender() - this.stats.markStart() + this.stats?.markStart() tween.update() - if (!window.freezeRender) { + if (!globalThis.freezeRender) { this.render(sizeChanged) } for (const fn of this.onRender) { fn(sizeChanged) } this.renderedFps++ - this.stats.markEnd() + this.stats?.markEnd() this.postRender() } @@ -154,10 +187,15 @@ export class DocumentRenderer { if (this.animationFrameId) { cancelAnimationFrame(this.animationFrameId) } - this.canvas.remove() - this.renderer.dispose() + if (this.timeoutId) { + clearTimeout(this.timeoutId) + } + if (this.canvas instanceof HTMLCanvasElement) { + this.canvas.remove() + } clearInterval(this.fpsInterval) - this.stats.dispose() + this.stats?.dispose() + this.renderer.dispose() } } @@ -250,3 +288,40 @@ class TopRightStats { this.statsGl.container.remove() } } + +const addCanvasToPage = () => { + const canvas = document.createElement('canvas') + canvas.id = 'viewer-canvas' + document.body.appendChild(canvas) + return canvas +} + +export const addCanvasForWorker = () => { + const canvas = addCanvasToPage() + const transferred = canvas.transferControlToOffscreen() + let removed = false + let onSizeChanged = (w, h) => { } + let oldSize = { width: 0, height: 0 } + const checkSize = () => { + if (removed) return + if (oldSize.width !== window.innerWidth || oldSize.height !== window.innerHeight) { + onSizeChanged(window.innerWidth, window.innerHeight) + oldSize = { width: window.innerWidth, height: window.innerHeight } + } + requestAnimationFrame(checkSize) + } + requestAnimationFrame(checkSize) + return { + canvas: transferred, + destroy () { + removed = true + canvas.remove() + }, + onSizeChanged (cb: (width: number, height: number) => void) { + onSizeChanged = cb + }, + get size () { + return { width: window.innerWidth, height: window.innerHeight } + } + } +} diff --git a/renderer/viewer/three/entities.ts b/renderer/viewer/three/entities.ts index 8a270332..0e8e6384 100644 --- a/renderer/viewer/three/entities.ts +++ b/renderer/viewer/three/entities.ts @@ -96,7 +96,7 @@ function getUsernameTexture ({ nameTagBackgroundColor = 'rgba(0, 0, 0, 0.3)', nameTagTextOpacity = 255 }: any, { fontFamily = 'sans-serif' }: any) { - const canvas = document.createElement('canvas') + const canvas = new OffscreenCanvas(64, 64) const ctx = canvas.getContext('2d') if (!ctx) throw new Error('Could not get 2d context') @@ -173,7 +173,7 @@ const nametags = {} const isFirstUpperCase = (str) => str.charAt(0) === str.charAt(0).toUpperCase() -function getEntityMesh (entity: import('prismarine-entity').Entity & { delete?: any; pos: any; name: any }, world: WorldRendererThree | undefined, options: { fontFamily: string }, overrides) { +function getEntityMesh (entity: import('prismarine-entity').Entity & { delete?: any; pos?: any; name?: any }, world: WorldRendererThree | undefined, options: { fontFamily: string }, overrides) { if (entity.name) { try { // https://github.com/PrismarineJS/prismarine-viewer/pull/410 @@ -209,6 +209,7 @@ export type SceneEntity = THREE.Object3D & { username?: string uuid?: string additionalCleanup?: () => void + originalEntity: import('prismarine-entity').Entity & { delete?; pos?, name } } export class Entities { @@ -250,6 +251,7 @@ export class Entities { constructor (public worldRenderer: WorldRendererThree) { this.debugMode = 'none' this.onSkinUpdate = () => { } + this.watchResourcesUpdates() } clear () { @@ -260,6 +262,20 @@ export class Entities { this.entities = {} } + reloadEntities () { + for (const entity of Object.values(this.entities)) { + // update all entities textures like held items, armour, etc + // todo update entity textures itself + this.update({ ...entity.originalEntity, delete: true, } as SceneEntity['originalEntity'], {}) + this.update(entity.originalEntity, {}) + } + } + + watchResourcesUpdates () { + this.worldRenderer.resourcesManager.on('assetsTexturesUpdated', () => this.reloadEntities()) + this.worldRenderer.resourcesManager.on('assetsInventoryReady', () => this.reloadEntities()) + } + setDebugMode (mode: string, entity: THREE.Object3D | null = null) { this.debugMode = mode for (const mesh of entity ? [entity] : Object.values(this.entities)) { @@ -291,7 +307,7 @@ export class Entities { const dt = this.clock.getDelta() const botPos = this.worldRenderer.viewerPosition - const VISIBLE_DISTANCE = 8 * 8 + const VISIBLE_DISTANCE = 10 * 10 for (const entityId of Object.keys(this.entities)) { const entity = this.entities[entityId] @@ -312,13 +328,8 @@ export class Entities { const dz = entity.position.z - botPos.z const distanceSquared = dx * dx + dy * dy + dz * dz - // Get chunk coordinates - const chunkX = Math.floor(entity.position.x / 16) * 16 - const chunkZ = Math.floor(entity.position.z / 16) * 16 - const chunkKey = `${chunkX},${chunkZ}` - - // Entity is visible if within 16 blocks OR in a finished chunk - entity.visible = !!(distanceSquared < VISIBLE_DISTANCE || this.worldRenderer.finishedChunks[chunkKey]) + // Entity is visible if within 20 blocks OR in a finished chunk + entity.visible = !!(distanceSquared < VISIBLE_DISTANCE || this.worldRenderer.shouldObjectVisible(entity)) this.maybeRenderPlayerSkin(entityId) } @@ -467,16 +478,16 @@ export class Entities { if (!playerObject) return try { - let playerCustomSkinImage: HTMLImageElement | undefined + let playerCustomSkinImage: ImageBitmap | undefined playerObject = this.getPlayerObject(entityId) if (!playerObject) return let skinTexture: THREE.Texture - let skinCanvas: HTMLCanvasElement + let skinCanvas: OffscreenCanvas if (skinUrl === stevePngUrl) { skinTexture = await steveTexture - const canvas = document.createElement('canvas') + const canvas = new OffscreenCanvas(64, 64) const ctx = canvas.getContext('2d') if (!ctx) throw new Error('Failed to get context') ctx.drawImage(skinTexture.image, 0, 0) @@ -550,6 +561,12 @@ export class Entities { } } + debugSwingArm () { + const playerObject = Object.values(this.entities).find(entity => entity.playerObject?.animation instanceof WalkingGeneralSwing) + if (!playerObject) return + (playerObject.playerObject!.animation as WalkingGeneralSwing).swingArm() + } + playAnimation (entityPlayerId, animation: 'walking' | 'running' | 'oneSwing' | 'idle' | 'crouch' | 'crouchWalking') { const playerObject = this.getPlayerObject(entityPlayerId) if (!playerObject) return @@ -594,7 +611,7 @@ export class Entities { if (previousModel && previousModel === textureUv?.modelName) return undefined if (textureUv && 'resolvedModel' in textureUv) { - const mesh = getBlockMeshFromModel(this.worldRenderer.material, textureUv.resolvedModel, textureUv.modelName, this.worldRenderer.resourcesManager.currentResources!.worldBlockProvider) + const mesh = getBlockMeshFromModel(this.worldRenderer.material, textureUv.resolvedModel, textureUv.modelName, this.worldRenderer.resourcesManager.currentResources.worldBlockProvider!) let SCALE = 1 if (specificProps['minecraft:display_context'] === 'ground') { SCALE = 0.5 @@ -675,7 +692,7 @@ export class Entities { } } - update (entity: import('prismarine-entity').Entity & { delete?; pos, name }, overrides) { + update (entity: SceneEntity['originalEntity'], overrides) { const justAdded = !this.entities[entity.id] const isPlayerModel = entity.name === 'player' @@ -703,9 +720,10 @@ export class Entities { return } - let mesh + let mesh: THREE.Object3D | undefined if (e === undefined) { - const group = new THREE.Group() + const group = new THREE.Group() as unknown as SceneEntity + group.originalEntity = entity if (entity.name === 'item' || entity.name === 'tnt' || entity.name === 'falling_block') { const item = entity.name === 'tnt' ? { name: 'tnt' } @@ -732,7 +750,7 @@ export class Entities { if (entity.name === 'item') { mesh.onBeforeRender = () => { const delta = clock.getDelta() - mesh.rotation.y += delta + mesh!.rotation.y += delta } } @@ -756,7 +774,6 @@ export class Entities { // } // } - //@ts-expect-error group.additionalCleanup = () => { // important: avoid texture memory leak and gpu slowdown object.itemsTexture?.dispose() @@ -795,7 +812,6 @@ export class Entities { wrapper.add(nameTag) } - //@ts-expect-error group.playerObject = playerObject wrapper.rotation.set(0, Math.PI, 0) mesh = wrapper @@ -808,7 +824,8 @@ export class Entities { if (!mesh) return mesh.name = 'mesh' // set initial position so there are no weird jumps update after - group.position.set(entity.pos.x, entity.pos.y, entity.pos.z) + const pos = entity.pos ?? entity.position + group.position.set(pos.x, pos.y, pos.z) // todo use width and height instead const boxHelper = new THREE.BoxHelper( @@ -856,7 +873,7 @@ export class Entities { //@ts-expect-error // set visibility const isInvisible = entity.metadata?.[0] & 0x20 - for (const child of mesh.children ?? []) { + for (const child of mesh!.children ?? []) { if (child.name !== 'nametag') { child.visible = !isInvisible } @@ -895,8 +912,8 @@ export class Entities { const hasArms = (parseInt(armorStandMeta.client_flags, 10) & 0x04) !== 0 const hasBasePlate = (parseInt(armorStandMeta.client_flags, 10) & 0x08) === 0 const isMarker = (parseInt(armorStandMeta.client_flags, 10) & 0x10) !== 0 - mesh.castShadow = !isMarker - mesh.receiveShadow = !isMarker + mesh!.castShadow = !isMarker + mesh!.receiveShadow = !isMarker if (isSmall) { e.scale.set(0.5, 0.5, 0.5) } else { @@ -965,7 +982,9 @@ export class Entities { // TODO: fix type // todo! fix errors in mc-data (no entities data prior 1.18.2) const item = (itemFrameMeta?.item ?? entity.metadata?.[8]) as any as { itemId, blockId, components, nbtData: { value: { map: { value: number } } } } - mesh.scale.set(1, 1, 1) + mesh!.scale.set(1, 1, 1) + mesh!.position.set(0, 0, -0.5) + e.rotation.x = -entity.pitch e.children.find(c => { if (c.name.startsWith('map_')) { @@ -982,25 +1001,33 @@ export class Entities { } return false })?.removeFromParent() + if (item && (item.itemId ?? item.blockId ?? 0) !== 0) { + // Get rotation from metadata, default to 0 if not present + // Rotation is stored in 45° increments (0-7) for items, 90° increments (0-3) for maps const rotation = (itemFrameMeta.rotation as any as number) ?? 0 const mapNumber = item.nbtData?.value?.map?.value ?? item.components?.find(x => x.type === 'map_id')?.data if (mapNumber) { // TODO: Use proper larger item frame model when a map exists - mesh.scale.set(16 / 12, 16 / 12, 1) + mesh!.scale.set(16 / 12, 16 / 12, 1) + // Handle map rotation (4 possibilities, 90° increments) this.addMapModel(e, mapNumber, rotation) } else { + // Handle regular item rotation (8 possibilities, 45° increments) const itemMesh = this.getItemMesh(item, { 'minecraft:display_context': 'fixed', }) if (itemMesh) { - itemMesh.mesh.position.set(0, 0, 0.43) + itemMesh.mesh.position.set(0, 0, -0.05) + // itemMesh.mesh.position.set(0, 0, 0.43) if (itemMesh.isBlock) { itemMesh.mesh.scale.set(0.25, 0.25, 0.25) } else { itemMesh.mesh.scale.set(0.5, 0.5, 0.5) } + // Rotate 180° around Y axis first itemMesh.mesh.rotateY(Math.PI) + // Then apply the 45° increment rotation itemMesh.mesh.rotateZ(-rotation * Math.PI / 4) itemMesh.mesh.name = 'item' e.add(itemMesh.mesh) @@ -1115,6 +1142,7 @@ export class Entities { } else { mapMesh.position.set(0, 0, 0.437) } + // Apply 90° increment rotation for maps (0-3) mapMesh.rotateZ(Math.PI * 2 - rotation * Math.PI / 2) mapMesh.name = `map_${mapNumber}` @@ -1267,7 +1295,7 @@ function addArmorModel (worldRenderer: WorldRendererThree, entityMesh: THREE.Obj if (!texturePath) { // TODO: Support mirroring on certain parts of the model const armorTextureName = `${armorMaterial}_layer_${layer}${overlay ? '_overlay' : ''}` - texturePath = worldRenderer.resourcesManager.currentResources!.customTextures.armor?.textures[armorTextureName]?.src ?? armorTextures[armorTextureName] + texturePath = worldRenderer.resourcesManager.currentResources.customTextures.armor?.textures[armorTextureName]?.src ?? armorTextures[armorTextureName] } if (!texturePath || !armorModel[slotType]) { removeArmorModel(entityMesh, slotType) diff --git a/renderer/viewer/three/entity/EntityMesh.ts b/renderer/viewer/three/entity/EntityMesh.ts index 2db09dd5..454bf35c 100644 --- a/renderer/viewer/three/entity/EntityMesh.ts +++ b/renderer/viewer/three/entity/EntityMesh.ts @@ -238,10 +238,11 @@ export function getMesh ( if (useBlockTexture) { if (!worldRenderer) throw new Error('worldRenderer is required for block textures') const blockName = texture.slice(6) - const textureInfo = worldRenderer.resourcesManager.currentResources!.blocksAtlasParser.getTextureInfo(blockName) + const textureInfo = worldRenderer.resourcesManager.currentResources.blocksAtlasJson.textures[blockName] if (textureInfo) { textureWidth = blocksTexture?.image.width ?? textureWidth textureHeight = blocksTexture?.image.height ?? textureHeight + // todo support su/sv textureOffset = [textureInfo.u, textureInfo.v] } else { console.error(`Unknown block ${blockName}`) @@ -546,4 +547,4 @@ export class EntityMesh { } } } -window.EntityMesh = EntityMesh +globalThis.EntityMesh = EntityMesh diff --git a/renderer/viewer/three/graphicsBackend.ts b/renderer/viewer/three/graphicsBackend.ts index 01bb7c56..5ea89b34 100644 --- a/renderer/viewer/three/graphicsBackend.ts +++ b/renderer/viewer/three/graphicsBackend.ts @@ -5,6 +5,7 @@ import { ProgressReporter } from '../../../src/core/progressReporter' import { showNotification } from '../../../src/react/NotificationProvider' import { displayEntitiesDebugList } from '../../playground/allEntitiesDebug' import supportedVersions from '../../../src/supportedVersions.mjs' +import { ResourcesManager } from '../../../src/resourcesManager' import { WorldRendererThree } from './worldrendererThree' import { DocumentRenderer } from './documentRenderer' import { PanoramaRenderer } from './panorama' @@ -12,7 +13,7 @@ import { initVR } from './world/vr' // https://discourse.threejs.org/t/updates-to-color-management-in-three-js-r152/50791 THREE.ColorManagement.enabled = false -window.THREE = THREE +globalThis.THREE = THREE const getBackendMethods = (worldRenderer: WorldRendererThree) => { return { @@ -24,7 +25,7 @@ const getBackendMethods = (worldRenderer: WorldRendererThree) => { updatePlayerSkin: worldRenderer.entities.updatePlayerSkin.bind(worldRenderer.entities), changeHandSwingingState: worldRenderer.changeHandSwingingState.bind(worldRenderer), getHighestBlocks: worldRenderer.getHighestBlocks.bind(worldRenderer), - rerenderAllChunks: worldRenderer.rerenderAllChunks.bind(worldRenderer), + reloadWorld: worldRenderer.reloadWorld.bind(worldRenderer), addMedia: worldRenderer.media.addMedia.bind(worldRenderer.media), destroyMedia: worldRenderer.media.destroyMedia.bind(worldRenderer.media), @@ -57,31 +58,27 @@ const createGraphicsBackend: GraphicsBackendLoader = (initOptions: GraphicsInitO let worldRenderer: WorldRendererThree | null = null const startPanorama = async () => { + if (!documentRenderer) throw new Error('Document renderer not initialized') if (worldRenderer) return - const qs = new URLSearchParams(window.location.search) + const qs = new URLSearchParams(location.search) if (qs.get('debugEntities')) { - initOptions.resourcesManager.currentConfig = { version: qs.get('version') || supportedVersions.at(-1)!, noInventoryGui: true } - await initOptions.resourcesManager.updateAssetsData({ }) + const fullResourceManager = initOptions.resourcesManager as ResourcesManager + fullResourceManager.currentConfig = { version: qs.get('version') || supportedVersions.at(-1)!, noInventoryGui: true } + await fullResourceManager.updateAssetsData({ }) - displayEntitiesDebugList(initOptions.resourcesManager.currentConfig.version) + displayEntitiesDebugList(fullResourceManager.currentConfig.version) return } if (!panoramaRenderer) { panoramaRenderer = new PanoramaRenderer(documentRenderer, initOptions, !!process.env.SINGLE_FILE_BUILD_MODE) - window.panoramaRenderer = panoramaRenderer + globalThis.panoramaRenderer = panoramaRenderer callModsMethod('panoramaCreated', panoramaRenderer) await panoramaRenderer.start() callModsMethod('panoramaReady', panoramaRenderer) } } - let version = '' - const prepareResources = async (ver: string, progressReporter: ProgressReporter): Promise<void> => { - version = ver - await initOptions.resourcesManager.updateAssetsData({ }) - } - const startWorld = async (displayOptions: DisplayWorldOptions) => { if (panoramaRenderer) { panoramaRenderer.dispose() diff --git a/renderer/viewer/three/holdingBlock.ts b/renderer/viewer/three/holdingBlock.ts index 6836d4f0..422076f0 100644 --- a/renderer/viewer/three/holdingBlock.ts +++ b/renderer/viewer/three/holdingBlock.ts @@ -5,7 +5,7 @@ import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBloc import { BlockModel } from 'mc-assets' import { getThreeBlockModelGroup, renderBlockThree, setBlockPosition } from '../lib/mesher/standaloneRenderer' import { getMyHand } from '../lib/hand' -import { IPlayerState, MovementState } from '../lib/basePlayerState' +import { MovementState, PlayerStateRenderer } from '../lib/basePlayerState' import { DebugGui } from '../lib/DebugGui' import { SmoothSwitcher } from '../lib/smoothSwitcher' import { watchProperty } from '../lib/utils/proxy' @@ -116,16 +116,22 @@ export default class HoldingBlock { offHandModeLegacy = false swingAnimator: HandSwingAnimator | undefined - playerState: IPlayerState + playerState: PlayerStateRenderer config: WorldRendererConfig constructor (public worldRenderer: WorldRendererThree, public offHand = false) { this.initCameraGroup() this.playerState = worldRenderer.displayOptions.playerState - this.playerState.events.on('heldItemChanged', (_, isOffHand) => { - if (this.offHand !== isOffHand) return - this.updateItem() - }) + this.worldRenderer.onReactivePlayerStateUpdated('heldItemMain', () => { + if (!this.offHand) { + this.updateItem() + } + }, false) + this.worldRenderer.onReactivePlayerStateUpdated('heldItemOff', () => { + if (this.offHand) { + this.updateItem() + } + }, false) this.config = worldRenderer.displayOptions.inWorldRenderingConfig this.offHandDisplay = this.offHand @@ -134,17 +140,21 @@ export default class HoldingBlock { // load default hand void getMyHand().then((hand) => { this.playerHand = hand + // trigger update + this.updateItem() }).then(() => { // now watch over the player skin watchProperty( async () => { - return getMyHand(this.playerState.reactive.playerSkin, this.playerState.onlineMode ? this.playerState.username : undefined) + return getMyHand(this.playerState.reactive.playerSkin, this.playerState.reactive.onlineMode ? this.playerState.reactive.username : undefined) }, this.playerState.reactive, 'playerSkin', (newHand) => { if (newHand) { this.playerHand = newHand + // trigger update + this.updateItem() } }, (oldHand) => { @@ -156,8 +166,8 @@ export default class HoldingBlock { } updateItem () { - if (!this.ready || !this.playerState.getHeldItem) return - const item = this.playerState.getHeldItem(this.offHand) + if (!this.ready) return + const item = this.offHand ? this.playerState.reactive.heldItemOff : this.playerState.reactive.heldItemMain if (item) { void this.setNewItem(item) } else if (this.offHand) { @@ -347,8 +357,8 @@ export default class HoldingBlock { itemId: handItem.id, }, { 'minecraft:display_context': 'firstperson', - 'minecraft:use_duration': this.playerState.getItemUsageTicks?.(), - 'minecraft:using_item': !!this.playerState.getItemUsageTicks?.(), + 'minecraft:use_duration': this.playerState.reactive.itemUsageTicks, + 'minecraft:using_item': !!this.playerState.reactive.itemUsageTicks, }, this.lastItemModelName) if (result) { const { mesh: itemMesh, isBlock, modelName } = result @@ -546,7 +556,7 @@ class HandIdleAnimator { private readonly debugGui: DebugGui - constructor (public handMesh: THREE.Object3D, public playerState: IPlayerState) { + constructor (public handMesh: THREE.Object3D, public playerState: PlayerStateRenderer) { this.handMesh = handMesh this.globalTime = 0 this.currentState = 'NOT_MOVING' @@ -700,7 +710,7 @@ class HandIdleAnimator { // Check for state changes from player state if (this.playerState) { - const newState = this.playerState.getMovementState() + const newState = this.playerState.reactive.movementState if (newState !== this.targetState) { this.setState(newState) } diff --git a/renderer/viewer/three/panorama.ts b/renderer/viewer/three/panorama.ts index ba1c4ac2..58a0fffa 100644 --- a/renderer/viewer/three/panorama.ts +++ b/renderer/viewer/three/panorama.ts @@ -6,8 +6,10 @@ import * as tweenJs from '@tweenjs/tween.js' import type { GraphicsInitOptions } from '../../../src/appViewer' import { WorldDataEmitter } from '../lib/worldDataEmitter' import { defaultWorldRendererConfig, WorldRendererCommon } from '../lib/worldrendererCommon' -import { BasePlayerState } from '../lib/basePlayerState' import { getDefaultRendererState } from '../baseGraphicsBackend' +import { loadThreeJsTextureFromUrl, loadThreeJsTextureFromUrlSync } from '../lib/utils/skins' +import { ResourcesManager } from '../../../src/resourcesManager' +import { getInitialPlayerStateRenderer } from '../lib/basePlayerState' import { WorldRendererThree } from './worldrendererThree' import { EntityMesh } from './entity/EntityMesh' import { DocumentRenderer } from './documentRenderer' @@ -48,7 +50,7 @@ export class PanoramaRenderer { this.directionalLight.castShadow = true this.scene.add(this.directionalLight) - this.camera = new THREE.PerspectiveCamera(85, window.innerWidth / window.innerHeight, 0.05, 1000) + this.camera = new THREE.PerspectiveCamera(85, this.documentRenderer.canvas.width / this.documentRenderer.canvas.height, 0.05, 1000) this.camera.position.set(0, 0, 0) this.camera.rotation.set(0, 0, 0) } @@ -63,47 +65,57 @@ export class PanoramaRenderer { this.documentRenderer.render = (sizeChanged = false) => { if (sizeChanged) { - this.camera.aspect = window.innerWidth / window.innerHeight + this.camera.aspect = this.documentRenderer.canvas.width / this.documentRenderer.canvas.height this.camera.updateProjectionMatrix() } this.documentRenderer.renderer.render(this.scene, this.camera) } } + async debugImageInFrontOfCamera () { + const image = await loadThreeJsTextureFromUrl(join('background', 'panorama_0.png')) + const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1000, 1000), new THREE.MeshBasicMaterial({ map: image })) + mesh.position.set(0, 0, -500) + mesh.rotation.set(0, 0, 0) + this.scene.add(mesh) + } + addClassicPanorama () { const panorGeo = new THREE.BoxGeometry(1000, 1000, 1000) - const loader = new THREE.TextureLoader() const panorMaterials = [] as THREE.MeshBasicMaterial[] const fadeInDuration = 200 - for (const file of panoramaFiles) { - // eslint-disable-next-line prefer-const - let material: THREE.MeshBasicMaterial + // void this.debugImageInFrontOfCamera() + + for (const file of panoramaFiles) { + const load = async () => { + const { texture } = loadThreeJsTextureFromUrlSync(join('background', file)) + + // Instead of using repeat/offset to flip, we'll use the texture matrix + texture.matrixAutoUpdate = false + texture.matrix.set( + -1, 0, 1, 0, 1, 0, 0, 0, 1 + ) + + texture.wrapS = THREE.ClampToEdgeWrapping + texture.wrapT = THREE.ClampToEdgeWrapping + texture.minFilter = THREE.LinearFilter + texture.magFilter = THREE.LinearFilter + + const material = new THREE.MeshBasicMaterial({ + map: texture, + transparent: true, + side: THREE.DoubleSide, + depthWrite: false, + opacity: 0 // Start with 0 opacity + }) - const texture = loader.load(join('background', file), () => { // Start fade-in when texture is loaded this.startTimes.set(material, Date.now()) - }) + panorMaterials.push(material) + } - // Instead of using repeat/offset to flip, we'll use the texture matrix - texture.matrixAutoUpdate = false - texture.matrix.set( - -1, 0, 1, 0, 1, 0, 0, 0, 1 - ) - - texture.wrapS = THREE.ClampToEdgeWrapping - texture.wrapT = THREE.ClampToEdgeWrapping - texture.minFilter = THREE.LinearFilter - texture.magFilter = THREE.LinearFilter - - material = new THREE.MeshBasicMaterial({ - map: texture, - transparent: true, - side: THREE.DoubleSide, - depthWrite: false, - opacity: 0 // Start with 0 opacity - }) - panorMaterials.push(material) + void load() } const panoramaBox = new THREE.Mesh(panorGeo, panorMaterials) @@ -145,8 +157,9 @@ export class PanoramaRenderer { async worldBlocksPanorama () { const version = '1.21.4' - this.options.resourcesManager.currentConfig = { version, noInventoryGui: true, } - await this.options.resourcesManager.updateAssetsData({ }) + const fullResourceManager = this.options.resourcesManager as ResourcesManager + fullResourceManager.currentConfig = { version, noInventoryGui: true, } + await fullResourceManager.updateAssetsData({ }) if (this.abortController.signal.aborted) return console.time('load panorama scene') const world = getSyncWorld(version) @@ -184,9 +197,9 @@ export class PanoramaRenderer { version, worldView, inWorldRenderingConfig: defaultWorldRendererConfig, - playerState: new BasePlayerState(), - rendererState: getDefaultRendererState(), - nonReactiveState: getDefaultRendererState() + playerState: getInitialPlayerStateRenderer(), + rendererState: getDefaultRendererState().reactive, + nonReactiveState: getDefaultRendererState().nonReactive } ) if (this.worldRenderer instanceof WorldRendererThree) { diff --git a/renderer/viewer/three/renderSlot.ts b/renderer/viewer/three/renderSlot.ts new file mode 100644 index 00000000..fd1eae91 --- /dev/null +++ b/renderer/viewer/three/renderSlot.ts @@ -0,0 +1,78 @@ +import { getRenamedData } from 'flying-squid/dist/blockRenames' +import { BlockModel } from 'mc-assets' +import { versionToNumber } from 'mc-assets/dist/utils' +import type { ResourcesManagerCommon } from '../../../src/resourcesManager' + +export type ResolvedItemModelRender = { + modelName: string, + originalItemName?: string +} + +export const renderSlot = (model: ResolvedItemModelRender, resourcesManager: ResourcesManagerCommon, debugIsQuickbar = false, fullBlockModelSupport = false): { + texture: string, + blockData?: Record<string, { slice, path }> & { resolvedModel: BlockModel }, + scale?: number, + slice?: number[], + modelName?: string, + image?: ImageBitmap +} | undefined => { + let itemModelName = model.modelName + const isItem = loadedData.itemsByName[itemModelName] + + // #region normalize item name + if (versionToNumber(bot.version) < versionToNumber('1.13')) itemModelName = getRenamedData(isItem ? 'items' : 'blocks', itemModelName, bot.version, '1.13.1') as string + // #endregion + + + let itemTexture + + if (!fullBlockModelSupport) { + const atlas = resourcesManager.currentResources?.guiAtlas?.json + // todo atlas holds all rendered blocks, not all possibly rendered item/block models, need to request this on demand instead (this is how vanilla works) + const tryGetAtlasTexture = (name?: string) => name && atlas?.textures[name.replace('minecraft:', '').replace('block/', '').replace('blocks/', '').replace('item/', '').replace('items/', '').replace('_inventory', '')] + const item = tryGetAtlasTexture(itemModelName) ?? tryGetAtlasTexture(model.originalItemName) + if (item) { + const x = item.u * atlas.width + const y = item.v * atlas.height + return { + texture: 'gui', + image: resourcesManager.currentResources!.guiAtlas!.image, + slice: [x, y, atlas.tileSize, atlas.tileSize], + scale: 0.25, + } + } + } + + const blockToTopTexture = (r) => r.top ?? r + + try { + if (!appViewer.resourcesManager.currentResources?.itemsRenderer) throw new Error('Items renderer is not available') + itemTexture = + appViewer.resourcesManager.currentResources.itemsRenderer.getItemTexture(itemModelName, {}, false, fullBlockModelSupport) + ?? (model.originalItemName ? appViewer.resourcesManager.currentResources.itemsRenderer.getItemTexture(model.originalItemName, {}, false, fullBlockModelSupport) : undefined) + ?? appViewer.resourcesManager.currentResources.itemsRenderer.getItemTexture('item/missing_texture')! + } catch (err) { + // get resourcepack from resource manager + reportError?.(`Failed to render item ${itemModelName} (original: ${model.originalItemName}) on ${bot.version} (resourcepack: TODO!): ${err.stack}`) + itemTexture = blockToTopTexture(appViewer.resourcesManager.currentResources!.itemsRenderer.getItemTexture('errored')!) + } + + itemTexture ??= blockToTopTexture(appViewer.resourcesManager.currentResources!.itemsRenderer.getItemTexture('unknown')!) + + + if ('type' in itemTexture) { + // is item + return { + texture: itemTexture.type, + slice: itemTexture.slice, + modelName: itemModelName + } + } else { + // is block + return { + texture: 'blocks', + blockData: itemTexture, + modelName: itemModelName + } + } +} diff --git a/renderer/viewer/three/world/cursorBlock.ts b/renderer/viewer/three/world/cursorBlock.ts index fe95c2c9..495a2d55 100644 --- a/renderer/viewer/three/world/cursorBlock.ts +++ b/renderer/viewer/three/world/cursorBlock.ts @@ -15,6 +15,7 @@ import destroyStage6 from '../../../../assets/destroy_stage_6.png' import destroyStage7 from '../../../../assets/destroy_stage_7.png' import destroyStage8 from '../../../../assets/destroy_stage_8.png' import destroyStage9 from '../../../../assets/destroy_stage_9.png' +import { loadThreeJsTextureFromUrl } from '../../lib/utils/skins' export class CursorBlock { _cursorLinesHidden = false @@ -36,17 +37,17 @@ export class CursorBlock { constructor (public readonly worldRenderer: WorldRendererThree) { // Initialize break mesh and textures - const loader = new THREE.TextureLoader() const destroyStagesImages = [ destroyStage0, destroyStage1, destroyStage2, destroyStage3, destroyStage4, destroyStage5, destroyStage6, destroyStage7, destroyStage8, destroyStage9 ] for (let i = 0; i < 10; i++) { - const texture = loader.load(destroyStagesImages[i]) - texture.magFilter = THREE.NearestFilter - texture.minFilter = THREE.NearestFilter - this.breakTextures.push(texture) + void loadThreeJsTextureFromUrl(destroyStagesImages[i]).then((texture) => { + texture.magFilter = THREE.NearestFilter + texture.minFilter = THREE.NearestFilter + this.breakTextures.push(texture) + }) } const breakMaterial = new THREE.MeshBasicMaterial({ diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index a8e08068..78381a29 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -3,17 +3,16 @@ import { Vec3 } from 'vec3' import nbt from 'prismarine-nbt' import PrismarineChatLoader from 'prismarine-chat' import * as tweenJs from '@tweenjs/tween.js' -import { subscribeKey } from 'valtio/utils' import { renderSign } from '../sign-renderer' -import { DisplayWorldOptions, GraphicsInitOptions, RendererReactiveState } from '../../../src/appViewer' +import { DisplayWorldOptions, GraphicsInitOptions } from '../../../src/appViewer' import { chunkPos, sectionPos } from '../lib/simpleUtils' import { WorldRendererCommon } from '../lib/worldrendererCommon' -import { addNewStat, removeAllStats } from '../lib/ui/newStats' +import { addNewStat } from '../lib/ui/newStats' import { MesherGeometryOutput } from '../lib/mesher/shared' import { ItemSpecificContextProperties } from '../lib/basePlayerState' import { getMyHand } from '../lib/hand' import { setBlockPosition } from '../lib/mesher/standaloneRenderer' -import { sendVideoPlay, sendVideoStop } from '../../../src/customChannels' +import { loadThreeJsTextureFromBitmap } from '../lib/utils/skins' import HoldingBlock from './holdingBlock' import { getMesh } from './entity/EntityMesh' import { armorModel } from './entity/armorModels' @@ -44,7 +43,7 @@ export class WorldRendererThree extends WorldRendererCommon { cameraGroupVr?: THREE.Object3D material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.1 }) itemsTexture: THREE.Texture - cursorBlock = new CursorBlock(this) + cursorBlock: CursorBlock onRender: Array<() => void> = [] cameraShake: CameraShake media: ThreeJsMedia @@ -82,8 +81,10 @@ export class WorldRendererThree extends WorldRendererCommon { if (!initOptions.resourcesManager) throw new Error('resourcesManager is required') super(initOptions.resourcesManager, displayOptions, initOptions) + this.renderer = renderer displayOptions.rendererState.renderer = WorldRendererThree.getRendererInfo(renderer) ?? '...' this.starField = new StarField(this.scene) + this.cursorBlock = new CursorBlock(this) this.holdingBlock = new HoldingBlock(this) this.holdingBlockLeft = new HoldingBlock(this, true) @@ -148,21 +149,21 @@ export class WorldRendererThree extends WorldRendererCommon { override watchReactivePlayerState () { super.watchReactivePlayerState() - this.onReactiveValueUpdated('inWater', (value) => { + this.onReactivePlayerStateUpdated('inWater', (value) => { this.scene.fog = value ? new THREE.Fog(0x00_00_ff, 0.1, this.displayOptions.playerState.reactive.waterBreathing ? 100 : 20) : null }) - this.onReactiveValueUpdated('ambientLight', (value) => { + this.onReactivePlayerStateUpdated('ambientLight', (value) => { if (!value) return this.ambientLight.intensity = value }) - this.onReactiveValueUpdated('directionalLight', (value) => { + this.onReactivePlayerStateUpdated('directionalLight', (value) => { if (!value) return this.directionalLight.intensity = value }) - this.onReactiveValueUpdated('lookingAtBlock', (value) => { + this.onReactivePlayerStateUpdated('lookingAtBlock', (value) => { this.cursorBlock.setHighlightCursorBlock(value ? new Vec3(value.x, value.y, value.z) : null, value?.shapes) }) - this.onReactiveValueUpdated('diggingBlock', (value) => { + this.onReactivePlayerStateUpdated('diggingBlock', (value) => { this.cursorBlock.updateBreakAnimation(value ? { x: value.x, y: value.y, z: value.z } : undefined, value?.stage ?? null, value?.mergedShape) }) } @@ -184,20 +185,18 @@ export class WorldRendererThree extends WorldRendererCommon { } async updateAssetsData (): Promise<void> { - const resources = this.resourcesManager.currentResources! + const resources = this.resourcesManager.currentResources const oldTexture = this.material.map const oldItemsTexture = this.itemsTexture - const texture = await new THREE.TextureLoader().loadAsync(resources.blocksAtlasParser.latestImage) - texture.magFilter = THREE.NearestFilter - texture.minFilter = THREE.NearestFilter + const texture = loadThreeJsTextureFromBitmap(resources.blocksAtlasImage) + texture.needsUpdate = true texture.flipY = false this.material.map = texture - const itemsTexture = await new THREE.TextureLoader().loadAsync(resources.itemsAtlasParser.latestImage) - itemsTexture.magFilter = THREE.NearestFilter - itemsTexture.minFilter = THREE.NearestFilter + const itemsTexture = loadThreeJsTextureFromBitmap(resources.itemsAtlasImage) + itemsTexture.needsUpdate = true itemsTexture.flipY = false this.itemsTexture = itemsTexture @@ -239,7 +238,7 @@ export class WorldRendererThree extends WorldRendererCommon { } getItemRenderData (item: Record<string, any>, specificProps: ItemSpecificContextProperties) { - return getItemUv(item, specificProps, this.resourcesManager) + return getItemUv(item, specificProps, this.resourcesManager, this.playerState) } async demoModel () { @@ -431,7 +430,7 @@ export class WorldRendererThree extends WorldRendererCommon { } setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number) { - const yOffset = this.displayOptions.playerState.getEyeHeight() + const yOffset = this.displayOptions.playerState.reactive.eyeHeight this.updateCamera(pos?.offset(0, yOffset, 0) ?? null, yaw, pitch) this.media.tryIntersectMedia() @@ -495,7 +494,8 @@ export class WorldRendererThree extends WorldRendererCommon { const sizeOrFovChanged = sizeChanged || this.displayOptions.inWorldRenderingConfig.fov !== this.camera.fov if (sizeOrFovChanged) { - this.camera.aspect = window.innerWidth / window.innerHeight + const size = this.renderer.getSize(new THREE.Vector2()) + this.camera.aspect = size.width / size.height this.camera.fov = this.displayOptions.inWorldRenderingConfig.fov this.camera.updateProjectionMatrix() } @@ -508,7 +508,7 @@ export class WorldRendererThree extends WorldRendererCommon { const cam = this.cameraGroupVr instanceof THREE.Group ? this.cameraGroupVr.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera this.renderer.render(this.scene, cam) - if (this.displayOptions.inWorldRenderingConfig.showHand && !this.playerState.shouldHideHand /* && !this.freeFlyMode */ && !this.renderer.xr.isPresenting) { + if (this.displayOptions.inWorldRenderingConfig.showHand && this.playerState.reactive.gameMode !== 'spectator' /* && !this.freeFlyMode */ && !this.renderer.xr.isPresenting) { this.holdingBlock.render(this.camera, this.renderer, this.ambientLight, this.directionalLight) this.holdingBlockLeft.render(this.camera, this.renderer, this.ambientLight, this.directionalLight) } @@ -710,6 +710,18 @@ export class WorldRendererThree extends WorldRendererCommon { super.destroy() } + shouldObjectVisible (object: THREE.Object3D) { + // Get chunk coordinates + const chunkX = Math.floor(object.position.x / 16) * 16 + const chunkZ = Math.floor(object.position.z / 16) * 16 + const sectionY = Math.floor(object.position.y / 16) * 16 + + const chunkKey = `${chunkX},${chunkZ}` + const sectionKey = `${chunkX},${sectionY},${chunkZ}` + + return !!this.finishedChunks[chunkKey] || !!this.sectionObjects[sectionKey] + } + updateSectionOffsets () { const currentTime = performance.now() for (const [key, anim] of Object.entries(this.sectionsOffsetsAnimations)) { @@ -756,6 +768,10 @@ export class WorldRendererThree extends WorldRendererCommon { } } } + + reloadWorld () { + this.entities.reloadEntities() + } } class StarField { diff --git a/src/appViewer.ts b/src/appViewer.ts index f4a21481..5ac0beaf 100644 --- a/src/appViewer.ts +++ b/src/appViewer.ts @@ -1,25 +1,26 @@ -import { WorldDataEmitter } from 'renderer/viewer/lib/worldDataEmitter' -import { BasePlayerState, IPlayerState } from 'renderer/viewer/lib/basePlayerState' +import { WorldDataEmitter, WorldDataEmitterWorker } from 'renderer/viewer/lib/worldDataEmitter' +import { PlayerStateRenderer } from 'renderer/viewer/lib/basePlayerState' import { subscribeKey } from 'valtio/utils' import { defaultWorldRendererConfig, WorldRendererConfig } from 'renderer/viewer/lib/worldrendererCommon' import { Vec3 } from 'vec3' import { SoundSystem } from 'renderer/viewer/three/threeJsSound' -import { proxy } from 'valtio' +import { proxy, subscribe } from 'valtio' import { getDefaultRendererState } from 'renderer/viewer/baseGraphicsBackend' import { getSyncWorld } from 'renderer/playground/shared' +import { MaybePromise } from 'contro-max/build/types/store' import { playerState } from './mineflayer/playerState' import { createNotificationProgressReporter, ProgressReporter } from './core/progressReporter' import { setLoadingScreenStatus } from './appStatus' import { activeModalStack, miscUiState } from './globalState' import { options } from './optionsStorage' -import { ResourcesManager } from './resourcesManager' +import { ResourcesManager, ResourcesManagerTransferred } from './resourcesManager' import { watchOptionsAfterWorldViewInit } from './watchOptions' export interface RendererReactiveState { world: { chunksLoaded: Set<string> + // chunksTotalNumber: number heightmaps: Map<string, Uint8Array> - chunksTotalNumber: number allChunksLoaded: boolean mesherWork: boolean intersectMedia: { id: string, x: number, y: number } | null @@ -31,9 +32,6 @@ export interface NonReactiveState { world: { chunksLoaded: Set<string> chunksTotalNumber: number - allChunksLoaded: boolean - mesherWork: boolean - intersectMedia: { id: string, x: number, y: number } | null } } @@ -42,33 +40,39 @@ export interface GraphicsBackendConfig { powerPreference?: 'high-performance' | 'low-power' statsVisible?: number sceneBackground: string + timeoutRendering?: boolean } const defaultGraphicsBackendConfig: GraphicsBackendConfig = { fpsLimit: undefined, powerPreference: undefined, - sceneBackground: 'lightblue' + sceneBackground: 'lightblue', + timeoutRendering: false } export interface GraphicsInitOptions<S = any> { - resourcesManager: ResourcesManager + resourcesManager: ResourcesManagerTransferred config: GraphicsBackendConfig rendererSpecificSettings: S - displayCriticalError: (error: Error) => void - setRendererSpecificSettings: (key: string, value: any) => void + callbacks: { + displayCriticalError: (error: Error) => void + setRendererSpecificSettings: (key: string, value: any) => void + + fireCustomEvent: (eventName: string, ...args: any[]) => void + } } export interface DisplayWorldOptions { version: string - worldView: WorldDataEmitter + worldView: WorldDataEmitterWorker inWorldRenderingConfig: WorldRendererConfig - playerState: IPlayerState + playerState: PlayerStateRenderer rendererState: RendererReactiveState nonReactiveState: NonReactiveState } -export type GraphicsBackendLoader = ((options: GraphicsInitOptions) => GraphicsBackend) & { +export type GraphicsBackendLoader = ((options: GraphicsInitOptions) => MaybePromise<GraphicsBackend>) & { id: string } @@ -108,8 +112,8 @@ export class AppViewer { inWorldRenderingConfig: WorldRendererConfig = proxy(defaultWorldRendererConfig) lastCamUpdate = 0 playerState = playerState - rendererState = proxy(getDefaultRendererState()) - nonReactiveState: NonReactiveState = getDefaultRendererState() + rendererState = proxy(getDefaultRendererState().reactive) + nonReactiveState: NonReactiveState = getDefaultRendererState().nonReactive worldReady: Promise<void> private resolveWorldReady: () => void @@ -133,19 +137,24 @@ export class AppViewer { rendererSpecificSettings[key.slice(rendererSettingsKey.length + 1)] = options[key] } } - const loaderOptions: GraphicsInitOptions = { - resourcesManager: this.resourcesManager, + const loaderOptions: GraphicsInitOptions = { // todo! + resourcesManager: this.resourcesManager as ResourcesManagerTransferred, config: this.config, - displayCriticalError (error) { - console.error(error) - setLoadingScreenStatus(error.message, true) + callbacks: { + displayCriticalError (error) { + console.error(error) + setLoadingScreenStatus(error.message, true) + }, + setRendererSpecificSettings (key: string, value: any) { + options[`${rendererSettingsKey}.${key}`] = value + }, + fireCustomEvent (eventName, ...args) { + // this.callbacks.fireCustomEvent(eventName, ...args) + } }, rendererSpecificSettings, - setRendererSpecificSettings (key: string, value: any) { - options[`${rendererSettingsKey}.${key}`] = value - } } - this.backend = loader(loaderOptions) + this.backend = await loader(loaderOptions) // if (this.resourcesManager.currentResources) { // void this.prepareResources(this.resourcesManager.currentResources.version, createNotificationProgressReporter()) @@ -156,9 +165,13 @@ export class AppViewer { const { method, args } = this.currentState this.backend[method](...args) if (method === 'startWorld') { + void this.worldView!.init(bot.entity.position) // void this.worldView!.init(args[0].playerState.getPosition()) } } + + // todo + modalStackUpdateChecks() } async startWithBot () { @@ -167,10 +180,10 @@ export class AppViewer { this.worldView!.listenToBot(bot) } - async startWorld (world, renderDistance: number, playerStateSend: IPlayerState = this.playerState) { + async startWorld (world, renderDistance: number, playerStateSend: PlayerStateRenderer = this.playerState) { if (this.currentDisplay === 'world') throw new Error('World already started') this.currentDisplay = 'world' - const startPosition = playerStateSend.getPosition() + const startPosition = bot.entity?.position ?? new Vec3(0, 64, 0) this.worldView = new WorldDataEmitter(world, renderDistance, startPosition) window.worldView = this.worldView watchOptionsAfterWorldViewInit(this.worldView) @@ -238,7 +251,8 @@ export class AppViewer { const { promise, resolve } = Promise.withResolvers<void>() this.worldReady = promise this.resolveWorldReady = resolve - this.rendererState = proxy(getDefaultRendererState()) + this.rendererState = proxy(getDefaultRendererState().reactive) + this.nonReactiveState = getDefaultRendererState().nonReactive // this.queuedDisplay = undefined } @@ -259,6 +273,7 @@ export class AppViewer { } } +// do not import this. Use global appViewer instead (without window prefix). export const appViewer = new AppViewer() window.appViewer = appViewer @@ -284,7 +299,7 @@ window.initialMenuStart = initialMenuStart const modalStackUpdateChecks = () => { // maybe start panorama - if (activeModalStack.length === 0 && !miscUiState.gameLoaded) { + if (!miscUiState.gameLoaded) { void initialMenuStart() } @@ -295,5 +310,4 @@ const modalStackUpdateChecks = () => { appViewer.inWorldRenderingConfig.foreground = activeModalStack.length === 0 } -subscribeKey(activeModalStack, 'length', modalStackUpdateChecks) -modalStackUpdateChecks() +subscribe(activeModalStack, modalStackUpdateChecks) diff --git a/src/browserfs.ts b/src/browserfs.ts index a4ae96cc..006b6db8 100644 --- a/src/browserfs.ts +++ b/src/browserfs.ts @@ -263,7 +263,7 @@ export const mountGoogleDriveFolder = async (readonly: boolean, rootId: string) return true } -export async function removeFileRecursiveAsync (path) { +export async function removeFileRecursiveAsync (path, removeDirectoryItself = true) { const errors = [] as Array<[string, Error]> try { const files = await fs.promises.readdir(path) @@ -282,7 +282,9 @@ export async function removeFileRecursiveAsync (path) { })) // After removing all files/directories, remove the current directory - await fs.promises.rmdir(path) + if (removeDirectoryItself) { + await fs.promises.rmdir(path) + } } catch (error) { errors.push([path, error]) } diff --git a/src/controls.ts b/src/controls.ts index f32cbda6..94364f2d 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -661,6 +661,9 @@ export const f3Keybinds: Array<{ localServer.players[0].world.columns = {} } void reloadChunks() + if (appViewer.backend?.backendMethods && typeof appViewer.backend.backendMethods.reloadWorld === 'function') { + appViewer.backend.backendMethods.reloadWorld() + } }, mobileTitle: 'Reload chunks', }, diff --git a/src/index.ts b/src/index.ts index bea10726..483abd74 100644 --- a/src/index.ts +++ b/src/index.ts @@ -74,7 +74,7 @@ import { showNotification } from './react/NotificationProvider' import { saveToBrowserMemory } from './react/PauseScreen' import './devReload' import './water' -import { ConnectOptions, getVersionAutoSelect, downloadOtherGameData, downloadAllMinecraftData } from './connect' +import { ConnectOptions, getVersionAutoSelect, downloadOtherGameData, downloadAllMinecraftData, loadMinecraftData } from './connect' import { ref, subscribe } from 'valtio' import { signInMessageState } from './react/SignInMessageProvider' import { findServerPassword, updateAuthenticatedAccountData, updateLoadedServerData, updateServerConnectionHistory } from './react/serversStorage' @@ -331,6 +331,7 @@ export async function connect (connectOptions: ConnectOptions) { await progress.executeWithMessage( 'Processing downloaded Minecraft data', async () => { + await loadMinecraftData(version) await appViewer.resourcesManager.loadSourceData(version) } ) @@ -448,17 +449,20 @@ export async function connect (connectOptions: ConnectOptions) { let newTokensCacheResult = null as any const cachedTokens = typeof connectOptions.authenticatedAccount === 'object' ? connectOptions.authenticatedAccount.cachedTokens : {} - const authData = connectOptions.authenticatedAccount ? await microsoftAuthflow({ - tokenCaches: cachedTokens, - proxyBaseUrl: connectOptions.proxy, - setProgressText (text) { - progress.setMessage(text) - }, - setCacheResult (result) { - newTokensCacheResult = result - }, - connectingServer: server.host - }) : undefined + let authData: Awaited<ReturnType<typeof microsoftAuthflow>> | undefined + if (connectOptions.authenticatedAccount) { + authData = await microsoftAuthflow({ + tokenCaches: cachedTokens, + proxyBaseUrl: connectOptions.proxy, + setProgressText (text) { + progress.setMessage(text) + }, + setCacheResult (result) { + newTokensCacheResult = result + }, + connectingServer: server.host + }) + } if (p2pMultiplayer) { clientDataStream = await connectToPeer(connectOptions.peerId!, connectOptions.peerOptions) @@ -569,6 +573,7 @@ export async function connect (connectOptions: ConnectOptions) { // "mapDownloader-saveInternal": false, // do not save into memory, todo must be implemeneted as we do really care of ram }) as unknown as typeof __type_bot window.bot = bot + if (connectOptions.viewerWsConnect) { void onBotCreatedViewerHandler() } @@ -691,6 +696,7 @@ export async function connect (connectOptions: ConnectOptions) { onBotCreate() bot.once('login', () => { + errorAbortController.abort() loadingTimerState.networkOnlyStart = 0 progress.setMessage('Loading world') }) @@ -708,7 +714,7 @@ export async function connect (connectOptions: ConnectOptions) { resolve() unsub() } else { - const perc = Math.round(appViewer.rendererState.world.chunksLoaded.size / appViewer.rendererState.world.chunksTotalNumber * 100) + const perc = Math.round(appViewer.rendererState.world.chunksLoaded.size / appViewer.nonReactiveState.world.chunksTotalNumber * 100) progress?.reportProgress('chunks', perc / 100) } }) @@ -727,9 +733,12 @@ export async function connect (connectOptions: ConnectOptions) { }) await appViewer.resourcesManager.promiseAssetsReady } - errorAbortController.abort() if (appStatusState.isError) return + if (!appViewer.resourcesManager.currentResources?.itemsRenderer) { + await appViewer.resourcesManager.updateAssetsData({}) + } + const loadWorldStart = Date.now() console.log('try to focus window') window.focus?.() @@ -741,7 +750,7 @@ export async function connect (connectOptions: ConnectOptions) { try { if (p2pConnectTimeout) clearTimeout(p2pConnectTimeout) - playerState.onlineMode = !!connectOptions.authenticatedAccount + playerState.reactive.onlineMode = !!connectOptions.authenticatedAccount progress.setMessage('Placing blocks (starting viewer)') if (!connectOptions.worldStateFileContents || connectOptions.worldStateFileContents.length < 3 * 1024 * 1024) { @@ -765,6 +774,9 @@ export async function connect (connectOptions: ConnectOptions) { console.log('bot spawned - starting viewer') await appViewer.startWorld(bot.world, renderDistance) appViewer.worldView!.listenToBot(bot) + if (appViewer.backend) { + void appViewer.worldView!.init(bot.entity.position) + } initMotionTracking() dayCycle() @@ -975,7 +987,7 @@ if (!reconnectOptions) { } }) - if (appQueryParams.serversList) { + if (appQueryParams.serversList && !appQueryParams.ip) { showModal({ reactType: 'serversList' }) } diff --git a/src/inventoryWindows.ts b/src/inventoryWindows.ts index 187bb997..d03b9fa4 100644 --- a/src/inventoryWindows.ts +++ b/src/inventoryWindows.ts @@ -10,7 +10,7 @@ import { versionToNumber } from 'renderer/viewer/common/utils' import { getRenamedData } from 'flying-squid/dist/blockRenames' import PrismarineChatLoader from 'prismarine-chat' import { BlockModel } from 'mc-assets' -import { activeGuiAtlas } from 'renderer/viewer/lib/guiRenderer' +import { renderSlot } from 'renderer/viewer/three/renderSlot' import Generic95 from '../assets/generic_95.png' import { appReplacableResources } from './generated/resources' import { activeModalStack, hideCurrentModal, hideModal, miscUiState, showModal } from './globalState' @@ -21,6 +21,7 @@ import { currentScaling } from './scaleInterface' import { getItemDescription } from './itemsDescriptions' import { MessageFormatPart } from './chatUtils' import { GeneralInputItem, getItemMetadata, getItemModelName, getItemNameRaw, RenderItem } from './mineflayer/items' +import { playerState } from './mineflayer/playerState' const loadedImagesCache = new Map<string, HTMLImageElement>() const cleanLoadedImagesCache = () => { @@ -134,8 +135,8 @@ export const onGameLoad = () => { const getImageSrc = (path): string | HTMLImageElement => { switch (path) { case 'gui/container/inventory': return appReplacableResources.latest_gui_container_inventory.content - case 'blocks': return appViewer.resourcesManager.currentResources!.blocksAtlasParser.latestImage - case 'items': return appViewer.resourcesManager.currentResources!.itemsAtlasParser.latestImage + case 'blocks': return appViewer.resourcesManager.blocksAtlasParser.latestImage + case 'items': return appViewer.resourcesManager.itemsAtlasParser.latestImage case 'gui/container/dispenser': return appReplacableResources.latest_gui_container_dispenser.content case 'gui/container/furnace': return appReplacableResources.latest_gui_container_furnace.content case 'gui/container/crafting_table': return appReplacableResources.latest_gui_container_crafting_table.content @@ -177,79 +178,6 @@ const getImage = ({ path = undefined as string | undefined, texture = undefined return loadedImagesCache.get(loadPath) } -export type ResolvedItemModelRender = { - modelName: string, - originalItemName?: string -} - -export const renderSlot = (model: ResolvedItemModelRender, debugIsQuickbar = false, fullBlockModelSupport = false): { - texture: string, - blockData?: Record<string, { slice, path }> & { resolvedModel: BlockModel }, - scale?: number, - slice?: number[], - modelName?: string, - image?: HTMLImageElement -} | undefined => { - let itemModelName = model.modelName - const isItem = loadedData.itemsByName[itemModelName] - - // #region normalize item name - if (versionToNumber(bot.version) < versionToNumber('1.13')) itemModelName = getRenamedData(isItem ? 'items' : 'blocks', itemModelName, bot.version, '1.13.1') as string - // #endregion - - - let itemTexture - - if (!fullBlockModelSupport) { - const atlas = activeGuiAtlas.atlas?.json - // todo atlas holds all rendered blocks, not all possibly rendered item/block models, need to request this on demand instead (this is how vanilla works) - const tryGetAtlasTexture = (name?: string) => name && atlas?.textures[name.replace('minecraft:', '').replace('block/', '').replace('blocks/', '').replace('item/', '').replace('items/', '').replace('_inventory', '')] - const item = tryGetAtlasTexture(itemModelName) ?? tryGetAtlasTexture(model.originalItemName) - if (item) { - const x = item.u * atlas.width - const y = item.v * atlas.height - return { - texture: 'gui', - image: activeGuiAtlas.atlas!.image, - slice: [x, y, atlas.tileSize, atlas.tileSize], - scale: 0.25, - } - } - } - - const blockToTopTexture = (r) => r.top ?? r - - try { - assertDefined(appViewer.resourcesManager.currentResources?.itemsRenderer) - itemTexture = - appViewer.resourcesManager.currentResources.itemsRenderer.getItemTexture(itemModelName, {}, false, fullBlockModelSupport) - ?? (model.originalItemName ? appViewer.resourcesManager.currentResources.itemsRenderer.getItemTexture(model.originalItemName, {}, false, fullBlockModelSupport) : undefined) - ?? appViewer.resourcesManager.currentResources.itemsRenderer.getItemTexture('item/missing_texture')! - } catch (err) { - inGameError(`Failed to render item ${itemModelName} (original: ${model.originalItemName}) on ${bot.version} (resourcepack: ${options.enabledResourcepack}): ${err.stack}`) - itemTexture = blockToTopTexture(appViewer.resourcesManager.currentResources!.itemsRenderer.getItemTexture('errored')!) - } - - itemTexture ??= blockToTopTexture(appViewer.resourcesManager.currentResources!.itemsRenderer.getItemTexture('unknown')!) - - - if ('type' in itemTexture) { - // is item - return { - texture: itemTexture.type, - slice: itemTexture.slice, - modelName: itemModelName - } - } else { - // is block - return { - texture: 'blocks', - blockData: itemTexture, - modelName: itemModelName - } - } -} - const getItemName = (slot: Item | RenderItem | null) => { const parsed = getItemNameRaw(slot, appViewer.resourcesManager) if (!parsed) return @@ -269,7 +197,7 @@ const itemToVisualKey = (slot: RenderItem | Item | null) => { slot['metadata'], slot.nbt ? JSON.stringify(slot.nbt) : '', slot['components'] ? JSON.stringify(slot['components']) : '', - activeGuiAtlas.version, + appViewer.resourcesManager.currentResources!.guiAtlasVersion, ].join('|') return keys } @@ -289,8 +217,8 @@ const mapSlots = (slots: Array<RenderItem | Item | null>, isJei = false) => { try { if (slot.durabilityUsed && slot.maxDurability) slot.durabilityUsed = Math.min(slot.durabilityUsed, slot.maxDurability) const debugIsQuickbar = !isJei && i === bot.inventory.hotbarStart + bot.quickBarSlot - const modelName = getItemModelName(slot, { 'minecraft:display_context': 'gui', }, appViewer.resourcesManager) - const slotCustomProps = renderSlot({ modelName, originalItemName: slot.name }, debugIsQuickbar) + const modelName = getItemModelName(slot, { 'minecraft:display_context': 'gui', }, appViewer.resourcesManager, playerState) + const slotCustomProps = renderSlot({ modelName, originalItemName: slot.name }, appViewer.resourcesManager, debugIsQuickbar) const itemCustomName = getItemName(slot) Object.assign(slot, { ...slotCustomProps, displayName: itemCustomName ?? slot.displayName }) //@ts-expect-error diff --git a/src/mineflayer/items.ts b/src/mineflayer/items.ts index 8a4e1fcd..495c4f39 100644 --- a/src/mineflayer/items.ts +++ b/src/mineflayer/items.ts @@ -1,11 +1,10 @@ import mojangson from 'mojangson' import nbt from 'prismarine-nbt' import { fromFormattedString } from '@xmcl/text-component' -import { ItemSpecificContextProperties } from 'renderer/viewer/lib/basePlayerState' +import { getItemSelector, ItemSpecificContextProperties, PlayerStateRenderer } from 'renderer/viewer/lib/basePlayerState' import { getItemDefinition } from 'mc-assets/dist/itemDefinitions' import { MessageFormatPart } from '../chatUtils' -import { ResourcesManager } from '../resourcesManager' -import { playerState } from './playerState' +import { ResourcesManager, ResourcesManagerCommon, ResourcesManagerTransferred } from '../resourcesManager' type RenderSlotComponent = { type: string, @@ -33,7 +32,7 @@ type PossibleItemProps = { display?: { Name?: JsonString } // {"text":"Knife","color":"white","italic":"true"} } -export const getItemMetadata = (item: GeneralInputItem, resourcesManager: ResourcesManager) => { +export const getItemMetadata = (item: GeneralInputItem, resourcesManager: ResourcesManagerCommon) => { let customText = undefined as string | any | undefined let customModel = undefined as string | undefined @@ -91,7 +90,7 @@ export const getItemMetadata = (item: GeneralInputItem, resourcesManager: Resour } -export const getItemNameRaw = (item: Pick<import('prismarine-item').Item, 'nbt'> | null, resourcesManager: ResourcesManager) => { +export const getItemNameRaw = (item: Pick<import('prismarine-item').Item, 'nbt'> | null, resourcesManager: ResourcesManagerCommon) => { if (!item) return '' const { customText } = getItemMetadata(item as GeneralInputItem, resourcesManager) if (!customText) return @@ -112,14 +111,14 @@ export const getItemNameRaw = (item: Pick<import('prismarine-item').Item, 'nbt'> } } -export const getItemModelName = (item: GeneralInputItem, specificProps: ItemSpecificContextProperties, resourcesManager: ResourcesManager) => { +export const getItemModelName = (item: GeneralInputItem, specificProps: ItemSpecificContextProperties, resourcesManager: ResourcesManagerCommon, playerState: PlayerStateRenderer) => { let itemModelName = item.name const { customModel } = getItemMetadata(item, resourcesManager) if (customModel) { itemModelName = customModel } - const itemSelector = playerState.getItemSelector({ + const itemSelector = getItemSelector(playerState, { ...specificProps }) const modelFromDef = getItemDefinition(appViewer.resourcesManager.itemsDefinitionsStore, { diff --git a/src/mineflayer/playerState.ts b/src/mineflayer/playerState.ts index 16739c86..f80a6971 100644 --- a/src/mineflayer/playerState.ts +++ b/src/mineflayer/playerState.ts @@ -1,46 +1,26 @@ -import { EventEmitter } from 'events' -import { Vec3 } from 'vec3' -import { BasePlayerState, IPlayerState, ItemSpecificContextProperties, MovementState, PlayerStateEvents } from 'renderer/viewer/lib/basePlayerState' import { HandItemBlock } from 'renderer/viewer/three/holdingBlock' -import TypedEmitter from 'typed-emitter' -import { ItemSelector } from 'mc-assets/dist/itemDefinitions' -import { proxy } from 'valtio' +import { getInitialPlayerState, PlayerStateRenderer } from 'renderer/viewer/lib/basePlayerState' +import { subscribe } from 'valtio' +import { subscribeKey } from 'valtio/utils' import { gameAdditionalState } from '../globalState' -export class PlayerStateManager implements IPlayerState { +/** + * can be used only in main thread. Mainly for more convenient reactive state updates. + * In renderer/ directory, use PlayerStateControllerRenderer type or worldRenderer.playerState. + */ +export class PlayerStateControllerMain implements PlayerStateRenderer { disableStateUpdates = false - private static instance: PlayerStateManager - readonly events = new EventEmitter() as TypedEmitter<PlayerStateEvents> - // Movement and physics state - private lastVelocity = new Vec3(0, 0, 0) - private movementState: MovementState = 'NOT_MOVING' private timeOffGround = 0 private lastUpdateTime = performance.now() // Held item state - private heldItem?: HandItemBlock - private offHandItem?: HandItemBlock - private itemUsageTicks = 0 private isUsingItem = false - private ready = false - public lightingDisabled = false - onlineMode = false - get username () { - return bot.username ?? '' - } + ready = false - reactive: IPlayerState['reactive'] = new BasePlayerState().reactive - - static getInstance (): PlayerStateManager { - if (!this.instance) { - this.instance = new PlayerStateManager() - } - return this.instance - } + reactive: PlayerStateRenderer['reactive'] constructor () { - this.updateState = this.updateState.bind(this) customEvents.on('mineflayerBotCreated', () => { this.ready = false bot.on('inject_allowed', () => { @@ -48,16 +28,27 @@ export class PlayerStateManager implements IPlayerState { this.ready = true this.botCreated() }) + bot.on('end', () => { + this.ready = false + }) }) } + private onBotCreatedOrGameJoined () { + this.reactive.username = bot.username ?? '' + } + private botCreated () { + console.log('bot created & plugins injected') + this.reactive = getInitialPlayerState() + this.onBotCreatedOrGameJoined() + const handleDimensionData = (data) => { let hasSkyLight = 1 try { hasSkyLight = data.dimension.value.has_skylight.value } catch {} - this.lightingDisabled = bot.game.dimension === 'the_nether' || bot.game.dimension === 'the_end' || !hasSkyLight + this.reactive.lightingDisabled = bot.game.dimension === 'the_nether' || bot.game.dimension === 'the_end' || !hasSkyLight } bot._client.on('login', (packet) => { @@ -68,7 +59,9 @@ export class PlayerStateManager implements IPlayerState { }) // Movement tracking - bot.on('move', this.updateState) + bot.on('move', () => { + this.updateMovementState() + }) // Item tracking bot.on('heldItemChanged', () => { @@ -77,8 +70,22 @@ export class PlayerStateManager implements IPlayerState { bot.inventory.on('updateSlot', (index) => { if (index === 45) this.updateHeldItem(true) }) + const updateSneakingOrFlying = () => { + this.updateMovementState() + this.reactive.sneaking = bot.controlState.sneak + this.reactive.flying = gameAdditionalState.isFlying + this.reactive.eyeHeight = bot.controlState.sneak && !gameAdditionalState.isFlying ? 1.27 : 1.62 + } bot.on('physicsTick', () => { - if (this.isUsingItem) this.itemUsageTicks++ + if (this.isUsingItem) this.reactive.itemUsageTicks++ + updateSneakingOrFlying() + }) + // todo move from gameAdditionalState to reactive directly + subscribeKey(gameAdditionalState, 'isSneaking', () => { + updateSneakingOrFlying() + }) + subscribeKey(gameAdditionalState, 'isFlying', () => { + updateSneakingOrFlying() }) // Initial held items setup @@ -89,14 +96,12 @@ export class PlayerStateManager implements IPlayerState { this.reactive.gameMode = bot.game.gameMode }) this.reactive.gameMode = bot.game?.gameMode - } - get shouldHideHand () { - return this.reactive.gameMode === 'spectator' + this.watchReactive() } // #region Movement and Physics State - private updateState () { + private updateMovementState () { if (!bot?.entity || this.disableStateUpdates) return const { velocity } = bot.entity @@ -109,7 +114,7 @@ export class PlayerStateManager implements IPlayerState { const deltaTime = now - this.lastUpdateTime this.lastUpdateTime = now - this.lastVelocity = velocity + // this.lastVelocity = velocity // Update time off ground if (isOnGround) { @@ -118,60 +123,26 @@ export class PlayerStateManager implements IPlayerState { this.timeOffGround += deltaTime } - if (this.isSneaking() || this.isFlying() || (this.timeOffGround > OFF_GROUND_THRESHOLD)) { - this.movementState = 'SNEAKING' + if (gameAdditionalState.isSneaking || gameAdditionalState.isFlying || (this.timeOffGround > OFF_GROUND_THRESHOLD)) { + this.reactive.movementState = 'SNEAKING' } else if (Math.abs(velocity.x) > VELOCITY_THRESHOLD || Math.abs(velocity.z) > VELOCITY_THRESHOLD) { - this.movementState = Math.abs(velocity.x) > SPRINTING_VELOCITY || Math.abs(velocity.z) > SPRINTING_VELOCITY + this.reactive.movementState = Math.abs(velocity.x) > SPRINTING_VELOCITY || Math.abs(velocity.z) > SPRINTING_VELOCITY ? 'SPRINTING' : 'WALKING' } else { - this.movementState = 'NOT_MOVING' + this.reactive.movementState = 'NOT_MOVING' } } - getMovementState (): MovementState { - return this.movementState - } - - getVelocity (): Vec3 { - return this.lastVelocity - } - - getEyeHeight (): number { - return bot.controlState.sneak && !this.isFlying() ? 1.27 : 1.62 - } - - isOnGround (): boolean { - return bot?.entity?.onGround ?? true - } - - isSneaking (): boolean { - return gameAdditionalState.isSneaking - } - - isFlying (): boolean { - return gameAdditionalState.isFlying - } - - isSprinting (): boolean { - return gameAdditionalState.isSprinting - } - - getPosition (): Vec3 { - return bot.entity?.position ?? new Vec3(0, 0, 0) - } - // #endregion - // #region Held Item State private updateHeldItem (isLeftHand: boolean) { const newItem = isLeftHand ? bot.inventory.slots[45] : bot.heldItem if (!newItem) { if (isLeftHand) { - this.offHandItem = undefined + this.reactive.heldItemOff = undefined } else { - this.heldItem = undefined + this.reactive.heldItemMain = undefined } - this.events.emit('heldItemChanged', undefined, isLeftHand) return } @@ -186,42 +157,36 @@ export class PlayerStateManager implements IPlayerState { } if (isLeftHand) { - this.offHandItem = item + this.reactive.heldItemOff = item } else { - this.heldItem = item + this.reactive.heldItemMain = item } - this.events.emit('heldItemChanged', item, isLeftHand) + // this.events.emit('heldItemChanged', item, isLeftHand) } startUsingItem () { if (this.isUsingItem) return this.isUsingItem = true - this.itemUsageTicks = 0 + this.reactive.itemUsageTicks = 0 } stopUsingItem () { this.isUsingItem = false - this.itemUsageTicks = 0 + this.reactive.itemUsageTicks = 0 } getItemUsageTicks (): number { - return this.itemUsageTicks + return this.reactive.itemUsageTicks } - getHeldItem (isLeftHand = false): HandItemBlock | undefined { - return isLeftHand ? this.offHandItem : this.heldItem + watchReactive () { + subscribeKey(this.reactive, 'eyeHeight', () => { + appViewer.backend?.updateCamera(bot.entity.position, bot.entity.yaw, bot.entity.pitch) + }) } - getItemSelector (specificProperties: ItemSpecificContextProperties, item?: import('prismarine-item').Item): ItemSelector['properties'] { - return { - ...specificProperties, - 'minecraft:date': new Date(), - // "minecraft:context_dimension": bot.entityp, - 'minecraft:time': bot.time.timeOfDay / 24_000, - } - } // #endregion } -export const playerState = PlayerStateManager.getInstance() +export const playerState = new PlayerStateControllerMain() window.playerState = playerState diff --git a/src/react/HotbarRenderApp.tsx b/src/react/HotbarRenderApp.tsx index 6e91180d..80f0a789 100644 --- a/src/react/HotbarRenderApp.tsx +++ b/src/react/HotbarRenderApp.tsx @@ -112,7 +112,7 @@ const HotbarInner = () => { inv.canvas.style.pointerEvents = 'auto' container.current.appendChild(inv.canvas) const upHotbarItems = () => { - if (!appViewer.resourcesManager.currentResources?.itemsAtlasParser) return + if (!appViewer.resourcesManager?.itemsAtlasParser) return upInventoryItems(true, inv) } diff --git a/src/react/Notification.tsx b/src/react/Notification.tsx index 7463c9a7..80354e83 100644 --- a/src/react/Notification.tsx +++ b/src/react/Notification.tsx @@ -1,14 +1,44 @@ import { motion, AnimatePresence } from 'framer-motion' -import PixelartIcon from './PixelartIcon' +import PixelartIcon, { pixelartIcons } from './PixelartIcon' +import { useUsingTouch } from './utilsApp' const duration = 0.2 // save pass: login -export default ({ type = 'message', message, subMessage = '', open, icon = '', action = undefined as (() => void) | undefined }) => { +const toastHeight = 32 + +interface NotificationProps { + open: boolean + message: string + type?: 'message' | 'error' | 'progress' + subMessage?: string + icon?: string + action?: () => void + topPosition?: number + + currentProgress?: number + totalProgress?: number +} + +export default ({ + type = 'message', + message, + subMessage = '', + open, + icon = '', + action = undefined as (() => void) | undefined, + topPosition = 0, + currentProgress, + totalProgress, +}: NotificationProps) => { + const isUsingTouch = useUsingTouch() const isError = type === 'error' icon ||= isError ? 'alert' : 'message' + const isLoader = type === 'progress' + + const top = (topPosition * toastHeight) + (isUsingTouch ? 18 : 0) // add space for mobile top buttons return <AnimatePresence> {open && ( <motion.div @@ -20,7 +50,7 @@ export default ({ type = 'message', message, subMessage = '', open, icon = '', a onClick={action} style={{ position: 'fixed', - top: 0, + top, right: 0, width: '180px', whiteSpace: 'nowrap', @@ -28,31 +58,54 @@ export default ({ type = 'message', message, subMessage = '', open, icon = '', a display: 'flex', gap: 4, alignItems: 'center', - padding: '3px 5px', + padding: '4px 5px', background: isError ? 'rgba(255, 0, 0, 0.7)' : 'rgba(0, 0, 0, 0.7)', - borderRadius: '0 0 0 5px', + borderRadius: top === 0 ? '0 0 0 5px' : '5px', pointerEvents: action ? 'auto' : 'none', - zIndex: 1200, + zIndex: isLoader ? 10 : 1200, }} > - <PixelartIcon iconName={icon} styles={{ fontSize: 12 }} /> + <PixelartIcon + iconName={icon} + styles={{ + fontSize: isLoader ? 15 : 12, + animation: isLoader ? 'rotation 6s linear infinite' : 'none', + }} + /> <div style={{ display: 'flex', flexDirection: 'column', - gap: 2, + width: '100%', }}> <div style={{ whiteSpace: 'normal', }}> - {message} + {translate(message)} </div> <div style={{ fontSize: '7px', whiteSpace: 'nowrap', color: 'lightgray', + marginTop: 3, }}> - {subMessage} + {translate(subMessage)} </div> + {currentProgress !== undefined && totalProgress !== undefined && ( + <div style={{ + width: '100%', + height: '2px', + background: 'rgba(128, 128, 128, 0.5)', + marginTop: '2px', + overflow: 'hidden', + }}> + <div style={{ + width: `${Math.min(100, (totalProgress ? currentProgress / totalProgress : 0) * 100)}%`, + height: '100%', + background: 'white', + transition: 'width 0.2s ease-out', + }} /> + </div> + )} </div> </motion.div> )} diff --git a/src/react/RendererDebugMenu.tsx b/src/react/RendererDebugMenu.tsx index 2e6030c1..f4bf7876 100644 --- a/src/react/RendererDebugMenu.tsx +++ b/src/react/RendererDebugMenu.tsx @@ -8,7 +8,11 @@ import Slider from './Slider' import styles from './rendererDebugMenu.module.css' export default () => { - const worldRenderer = window.world as WorldRendererCommon + const worldRenderer = window.world as WorldRendererCommon | undefined + return worldRenderer ? <RendererDebugMenu worldRenderer={worldRenderer} /> : null +} + +const RendererDebugMenu = ({ worldRenderer }: { worldRenderer: WorldRendererCommon }) => { const { reactiveDebugParams } = worldRenderer const { chunksRenderAboveEnabled, chunksRenderBelowEnabled, chunksRenderDistanceEnabled, chunksRenderAboveOverride, chunksRenderBelowOverride, chunksRenderDistanceOverride, stopRendering, disableEntities } = useSnapshot(reactiveDebugParams) diff --git a/src/react/Singleplayer.tsx b/src/react/Singleplayer.tsx index 872a21ce..26d6427c 100644 --- a/src/react/Singleplayer.tsx +++ b/src/react/Singleplayer.tsx @@ -80,7 +80,7 @@ const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus, }} onDoubleClick={() => onInteraction?.('enter')} > - <img className={`${styles.world_image} ${iconSrc ? '' : styles.image_missing}`} src={iconSrc ?? missingWorldPreview} alt='world preview' /> + <img className={`${styles.world_image} ${iconSrc ? '' : styles.image_missing}`} src={iconSrc ?? missingWorldPreview} alt='' /> <div className={styles.world_info}> <div className={styles.world_title}> <div>{title}</div> diff --git a/src/rendererUtils.ts b/src/rendererUtils.ts index 49a77801..0a49fc78 100644 --- a/src/rendererUtils.ts +++ b/src/rendererUtils.ts @@ -13,7 +13,7 @@ const BASE_MOVEMENT_SPEED = 0.1 // Default walking speed in Minecraft const FOV_EFFECT_SCALE = 1 // Equivalent to Minecraft's FOV Effects slider const updateFovAnimation = () => { - if (!bot) return + if (!playerState.ready) return // Calculate base FOV modifier let fovModifier = 1 @@ -39,10 +39,10 @@ const updateFovAnimation = () => { } // Item usage modifier - if (playerState.getHeldItem()) { - const heldItem = playerState.getHeldItem() - if (heldItem?.name === 'bow' && playerState.getItemUsageTicks() > 0) { - const ticksUsingItem = playerState.getItemUsageTicks() + if (playerState.reactive.heldItemMain) { + const heldItem = playerState.reactive.heldItemMain + if (heldItem?.name === 'bow' && playerState.reactive.itemUsageTicks > 0) { + const ticksUsingItem = playerState.reactive.itemUsageTicks let usageProgress = ticksUsingItem / 20 if (usageProgress > 1) { usageProgress = 1 @@ -88,8 +88,4 @@ export const watchFov = () => { customEvents.on('gameLoaded', () => { updateFovAnimation() }) - - subscribeKey(gameAdditionalState, 'isSneaking', () => { - appViewer.backend?.updateCamera(bot.entity.position, bot.entity.yaw, bot.entity.pitch) - }) } diff --git a/src/resourcePack.ts b/src/resourcePack.ts index 7c348cb2..e0f0fca4 100644 --- a/src/resourcePack.ts +++ b/src/resourcePack.ts @@ -32,7 +32,7 @@ const getLoadedImage = async (url: string) => { const resourcepackPackBasePath = '/data/resourcePacks/' export const uninstallResourcePack = async (name = 'default') => { if (await existsAsync('/resourcepack/pack.mcmeta')) { - await removeFileRecursiveAsync('/resourcepack') + await removeFileRecursiveAsync('/resourcepack', false) gameAdditionalState.usingServerResourcePack = false } const basePath = resourcepackPackBasePath + name @@ -212,7 +212,6 @@ export const getResourcepackTiles = async (type: 'blocks' | 'items' | 'armor', e if (!basePath) return let firstTextureSize: number | undefined const namespaces = await fs.promises.readdir(join(basePath, 'assets')) - progressReporter.beginStage(`generate-atlas-texture-${type}`, `Generating atlas texture for ${type}`) const textures = {} as Record<string, HTMLImageElement> let path @@ -420,6 +419,7 @@ const prepareBlockstatesAndModels = async (progressReporter: ProgressReporter) = } } catch (err) { console.error('Failed to read some of resource pack blockstates and models', err) + currentErrors.push('Failed to read blockstates/models') resources.customBlockStates = undefined resources.customModels = undefined resources.customItemModelNames = {} @@ -439,8 +439,10 @@ const downloadAndUseResourcePack = async (url: string, progressReporter: Progres console.log('Downloading server resource pack', url) console.time('downloadServerResourcePack') const response = await fetch(url).catch((err) => { - console.log(`Ensure server on ${url} support CORS which is not required for regular client, but is required for the web client`) console.error(err) + if (err.message === 'Failed to fetch') { + err.message = `Check internet connection and ensure server on ${url} support CORS which is not required for the vanilla client, but is required for the web client.` + } progressReporter.error('Failed to download resource pack: ' + err.message) }) console.timeEnd('downloadServerResourcePack') @@ -475,6 +477,7 @@ const downloadAndUseResourcePack = async (url: string, progressReporter: Progres showNotification('Failed to install resource pack: ' + err.message) }) } catch (err) { + console.error('Could not install resource pack', err) progressReporter.error('Could not install resource pack: ' + err.message) } finally { progressReporter.endStage('download-resource-pack') @@ -513,21 +516,19 @@ export const onAppLoad = () => { cancel: !forced, minecraftJsonMessage: promptMessagePacket, }) - if (Date.now() - start < 700) { // wait for state protocol switch - await new Promise(resolve => { + if (Date.now() - start < 700) { + void new Promise(resolve => { + // wait for state protocol switch setTimeout(resolve, 700) + }).then(() => { + if (choice === false || choice === 'Pretend Installed (not recommended)' || choice === 'Download & Install (recommended)' || choice) { + console.log('accepting resource pack') + bot.acceptResourcePack() + } else { + bot.denyResourcePack() + } }) } - if (choice === false) { - bot.acceptResourcePack() - return - } - if (!choice) { - bot.denyResourcePack() - return - } - console.log('accepting resource pack') - bot.acceptResourcePack() if (choice === true || choice === 'Download & Install (recommended)') { await downloadAndUseResourcePack(packet.url, createFullScreenProgressReporter()).catch((err) => { console.error(err) @@ -590,10 +591,17 @@ const updateTextures = async (progressReporter = createConsoleLogProgressReporte const origItemsFiles = Object.keys(appViewer.resourcesManager.sourceItemsAtlases.latest.textures) const origArmorFiles = Object.keys(armorTextures) const { usedBlockTextures, usedItemTextures } = await prepareBlockstatesAndModels(progressReporter) ?? {} - const blocksData = await getResourcepackTiles('blocks', [...origBlocksFiles, ...usedBlockTextures ?? []], progressReporter) - const itemsData = await getResourcepackTiles('items', [...origItemsFiles, ...usedItemTextures ?? []], progressReporter) - const armorData = await getResourcepackTiles('armor', origArmorFiles, progressReporter) - await updateAllReplacableTextures() + progressReporter.beginStage(`generate-atlas-texture-blocks`, `Generating atlas textures`) + const [ + blocksData, + itemsData, + armorData + ] = await Promise.all([ + getResourcepackTiles('blocks', [...origBlocksFiles, ...usedBlockTextures ?? []], progressReporter), + getResourcepackTiles('items', [...origItemsFiles, ...usedItemTextures ?? []], progressReporter), + getResourcepackTiles('armor', origArmorFiles, progressReporter), + updateAllReplacableTextures() + ]) resources.customTextures = {} if (blocksData) { diff --git a/src/resourcesManager.ts b/src/resourcesManager.ts index 4ee91ab4..ef3dd7ce 100644 --- a/src/resourcesManager.ts +++ b/src/resourcesManager.ts @@ -8,26 +8,25 @@ import blocksAtlasLegacy from 'mc-assets/dist/blocksAtlasLegacy.png' import itemsAtlasLatest from 'mc-assets/dist/itemsAtlasLatest.png' import itemsAtlasLegacy from 'mc-assets/dist/itemsAtlasLegacy.png' import christmasPack from 'mc-assets/dist/textureReplacements/christmas' -import { AtlasParser } from 'mc-assets/dist/atlasParser' +import { AtlasParser, ItemsAtlasesOutputJson } from 'mc-assets/dist/atlasParser' import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider' import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer' import { getLoadedItemDefinitionsStore } from 'mc-assets' -import { getLoadedImage } from 'mc-assets/dist/utils' import { generateGuiAtlas } from 'renderer/viewer/lib/guiRenderer' import { importLargeData } from '../generated/large-data-aliases' -import { loadMinecraftData } from './connect' type ResourceManagerEvents = { assetsTexturesUpdated: () => void + assetsInventoryStarted: () => void assetsInventoryReady: () => void } -export class LoadedResources { +export class LoadedResourcesTransferrable { + allReady = false // Atlas parsers - itemsAtlasParser: AtlasParser - blocksAtlasParser: AtlasParser - itemsAtlasImage: HTMLImageElement - blocksAtlasImage: HTMLImageElement + itemsAtlasImage: ImageBitmap + blocksAtlasImage: ImageBitmap + blocksAtlasJson: ItemsAtlasesOutputJson // User data (specific to current resourcepack/version) customBlockStates?: Record<string, any> customModels?: Record<string, any> @@ -38,9 +37,11 @@ export class LoadedResources { blocks?: { tileSize: number | undefined, textures: Record<string, HTMLImageElement> } armor?: { tileSize: number | undefined, textures: Record<string, HTMLImageElement> } } = {} + guiAtlas: { json: any, image: ImageBitmap } | null = null + guiAtlasVersion = 0 itemsRenderer: ItemsRenderer - worldBlockProvider: WorldBlockProvider + worldBlockProvider?: WorldBlockProvider blockstatesModels: any = null version: string @@ -59,8 +60,17 @@ export interface UpdateAssetsRequest { _?: false } +export interface ResourcesManagerTransferred extends TypedEmitter<ResourceManagerEvents> { + currentResources: LoadedResourcesTransferrable +} +export interface ResourcesManagerCommon extends TypedEmitter<ResourceManagerEvents> { + currentResources: LoadedResourcesTransferrable | undefined +} + const STABLE_MODELS_VERSION = '1.21.4' export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<ResourceManagerEvents>) { + static restorerName = 'ResourcesManager' + // Source data (imported, not changing) sourceBlockStatesModels: any = null readonly sourceBlocksAtlases: any = blocksAtlases @@ -68,7 +78,9 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<Re readonly sourceItemDefinitionsJson: any = itemDefinitionsJson readonly itemsDefinitionsStore = getLoadedItemDefinitionsStore(this.sourceItemDefinitionsJson) - currentResources: LoadedResources | undefined + currentResources: LoadedResourcesTransferrable | undefined + itemsAtlasParser: AtlasParser + blocksAtlasParser: AtlasParser currentConfig: ResourcesCurrentConfig | undefined abortController = new AbortController() _promiseAssetsReadyResolvers = Promise.withResolvers<void>() @@ -76,17 +88,12 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<Re return this._promiseAssetsReadyResolvers.promise } - async loadMcData (version: string) { - await loadMinecraftData(version) - } - async loadSourceData (version: string) { - await this.loadMcData(version) this.sourceBlockStatesModels ??= await importLargeData('blockStatesModels') } resetResources () { - this.currentResources = new LoadedResources() + this.currentResources = new LoadedResourcesTransferrable() } async updateAssetsData (request: UpdateAssetsRequest, unstableSkipEvent = false) { @@ -96,7 +103,7 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<Re await this.loadSourceData(this.currentConfig.version) if (abortController.signal.aborted) return - const resources = this.currentResources ?? new LoadedResources() + const resources = this.currentResources ?? new LoadedResourcesTransferrable() resources.version = this.currentConfig.version resources.texturesVersion = this.currentConfig.texturesVersion ?? resources.version @@ -115,41 +122,28 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<Re ...resources.customModels } - await this.recreateBlockAtlas(resources) + console.time('recreateAtlases') + await Promise.all([ + this.recreateBlockAtlas(resources), + this.recreateItemsAtlas(resources) + ]) + console.timeEnd('recreateAtlases') if (abortController.signal.aborted) return - const itemsAssetsParser = new AtlasParser(this.sourceItemsAtlases, itemsAtlasLatest, itemsAtlasLegacy) - const customItemTextures = Object.keys(resources.customTextures.items?.textures ?? {}) - console.time('createItemsAtlas') - const { atlas: itemsAtlas, canvas: itemsCanvas } = await itemsAssetsParser.makeNewAtlas( - resources.texturesVersion, - (textureName) => { - const texture = resources.customTextures.items?.textures[textureName] - if (!texture) return - return texture - }, - resources.customTextures.items?.tileSize, - undefined, - customItemTextures - ) - console.timeEnd('createItemsAtlas') - - resources.itemsAtlasParser = new AtlasParser({ latest: itemsAtlas }, itemsCanvas.toDataURL()) - resources.itemsAtlasImage = await getLoadedImage(itemsCanvas.toDataURL()) - - if (resources.version && resources.blockstatesModels && resources.itemsAtlasParser && resources.blocksAtlasParser) { + if (resources.version && resources.blockstatesModels && this.itemsAtlasParser && this.blocksAtlasParser) { resources.itemsRenderer = new ItemsRenderer( resources.version, resources.blockstatesModels, - resources.itemsAtlasParser, - resources.blocksAtlasParser + this.itemsAtlasParser, + this.blocksAtlasParser ) } if (abortController.signal.aborted) return this.currentResources = resources + resources.allReady = true if (!unstableSkipEvent) { // todo rework resourcepack optimization this.emit('assetsTexturesUpdated') } @@ -157,6 +151,7 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<Re if (this.currentConfig.noInventoryGui) { this._promiseAssetsReadyResolvers.resolve() } else { + this.emit('assetsInventoryStarted') void this.generateGuiTextures().then(() => { if (abortController.signal.aborted) return if (!unstableSkipEvent) { @@ -167,7 +162,7 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<Re } } - async recreateBlockAtlas (resources: LoadedResources = this.currentResources!) { + async recreateBlockAtlas (resources: LoadedResourcesTransferrable = this.currentResources!) { const blockTexturesChanges = {} as Record<string, string> const date = new Date() if ((date.getMonth() === 11 && date.getDate() >= 24) || (date.getMonth() === 0 && date.getDate() <= 6)) { @@ -194,16 +189,36 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<Re ) console.timeEnd('createBlocksAtlas') - resources.blocksAtlasParser = new AtlasParser({ latest: blocksAtlas }, blocksCanvas.toDataURL()) - resources.blocksAtlasImage = await getLoadedImage(blocksCanvas.toDataURL()) + this.blocksAtlasParser = new AtlasParser({ latest: blocksAtlas }, blocksCanvas.toDataURL()) + resources.blocksAtlasImage = await createImageBitmap(blocksCanvas) + resources.blocksAtlasJson = this.blocksAtlasParser.atlas.latest resources.worldBlockProvider = worldBlockProvider( resources.blockstatesModels, - resources.blocksAtlasParser.atlas, + this.blocksAtlasParser.atlas, STABLE_MODELS_VERSION ) } + async recreateItemsAtlas (resources: LoadedResourcesTransferrable = this.currentResources!) { + const itemsAssetsParser = new AtlasParser(this.sourceItemsAtlases, itemsAtlasLatest, itemsAtlasLegacy) + const customItemTextures = Object.keys(resources.customTextures.items?.textures ?? {}) + const { atlas: itemsAtlas, canvas: itemsCanvas } = await itemsAssetsParser.makeNewAtlas( + resources.texturesVersion, + (textureName) => { + const texture = resources.customTextures.items?.textures[textureName] + if (!texture) return + return texture + }, + resources.customTextures.items?.tileSize, + undefined, + customItemTextures + ) + + this.itemsAtlasParser = new AtlasParser({ latest: itemsAtlas }, itemsCanvas.toDataURL()) + resources.itemsAtlasImage = await createImageBitmap(itemsCanvas) + } + async generateGuiTextures () { await generateGuiAtlas() } @@ -211,7 +226,7 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<Re async downloadDebugAtlas (isItems = false) { const resources = this.currentResources if (!resources) throw new Error('No resources loaded') - const atlasParser = (isItems ? resources.itemsAtlasParser : resources.blocksAtlasParser)! + const atlasParser = (isItems ? this.itemsAtlasParser : this.blocksAtlasParser)! const dataUrl = await atlasParser.createDebugImage(true) const a = document.createElement('a') a.href = dataUrl From 1b20845ed5a42bc8ebe98a096348e100ad4aa2ec Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Wed, 18 Jun 2025 08:37:13 +0300 Subject: [PATCH 032/181] fix initial component mount sometimes displays not found modal the reason why it happens is known --- src/react/NoModalFoundProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/NoModalFoundProvider.tsx b/src/react/NoModalFoundProvider.tsx index 4ca57830..f21045ba 100644 --- a/src/react/NoModalFoundProvider.tsx +++ b/src/react/NoModalFoundProvider.tsx @@ -32,7 +32,7 @@ export default () => { const { enabled } = useSnapshot(componentActive) const lastModal = useSnapshot(activeModalStack).at(-1)?.reactType - if (!enabled) return null + if (!enabled || watchedModalsFromHooks.value.includes(lastModal!)) return null return <Screen title={`Error: Modal (route) ${lastModal} is is unavailable or doesn't exist`} style={{ From f126f56844421ea5d1b704fce2f987709793e8fa Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Wed, 18 Jun 2025 16:57:03 +0300 Subject: [PATCH 033/181] fix: fix visual gaps between blocks of water! --- renderer/viewer/lib/mesher/models.ts | 19 ++++++------- renderer/viewer/lib/worldDataEmitter.ts | 4 ++- src/appViewer.ts | 36 ++++++++++++++++--------- src/reactUi.tsx | 12 ++++++--- 4 files changed, 45 insertions(+), 26 deletions(-) diff --git a/renderer/viewer/lib/mesher/models.ts b/renderer/viewer/lib/mesher/models.ts index 3658d120..c2075764 100644 --- a/renderer/viewer/lib/mesher/models.ts +++ b/renderer/viewer/lib/mesher/models.ts @@ -132,7 +132,7 @@ const getVec = (v: Vec3, dir: Vec3) => { return v.plus(dir) } -function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, type: number, biome: string, water: boolean, attr: Record<string, any>, isRealWater: boolean) { +function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, type: number, biome: string, water: boolean, attr: MesherGeometryOutput, isRealWater: boolean) { const heights: number[] = [] for (let z = -1; z <= 1; z++) { for (let x = -1; x <= 1; x++) { @@ -192,13 +192,14 @@ function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, typ for (const pos of corners) { const height = cornerHeights[pos[2] * 2 + pos[0]] - attr.t_positions.push( - (pos[0] ? 0.999 : 0.001) + (cursor.x & 15) - 8, - (pos[1] ? height - 0.001 : 0.001) + (cursor.y & 15) - 8, - (pos[2] ? 0.999 : 0.001) + (cursor.z & 15) - 8 + const OFFSET = 0.0001 + attr.t_positions!.push( + (pos[0] ? 1 - OFFSET : OFFSET) + (cursor.x & 15) - 8, + (pos[1] ? height - OFFSET : OFFSET) + (cursor.y & 15) - 8, + (pos[2] ? 1 - OFFSET : OFFSET) + (cursor.z & 15) - 8 ) - attr.t_normals.push(...dir) - attr.t_uvs.push(pos[3] * su + u, pos[4] * sv * (pos[1] ? 1 : height) + v) + attr.t_normals!.push(...dir) + attr.t_uvs!.push(pos[3] * su + u, pos[4] * sv * (pos[1] ? 1 : height) + v) let cornerLightResult = baseLight if (world.config.smoothLighting) { @@ -223,7 +224,7 @@ function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, typ } // Apply light value to tint - attr.t_colors.push(tint[0] * cornerLightResult, tint[1] * cornerLightResult, tint[2] * cornerLightResult) + attr.t_colors!.push(tint[0] * cornerLightResult, tint[1] * cornerLightResult, tint[2] * cornerLightResult) } } } @@ -487,7 +488,7 @@ const isBlockWaterlogged = (block: Block) => { } let unknownBlockModel: BlockModelPartsResolved -export function getSectionGeometry (sx, sy, sz, world: World) { +export function getSectionGeometry (sx: number, sy: number, sz: number, world: World) { let delayedRender = [] as Array<() => void> const attr: MesherGeometryOutput = { diff --git a/renderer/viewer/lib/worldDataEmitter.ts b/renderer/viewer/lib/worldDataEmitter.ts index 86f372d1..435d9183 100644 --- a/renderer/viewer/lib/worldDataEmitter.ts +++ b/renderer/viewer/lib/worldDataEmitter.ts @@ -197,7 +197,9 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo async init (pos: Vec3) { this.updateViewDistance(this.viewDistance) this.emitter.emit('chunkPosUpdate', { pos }) - this.emitter.emit('time', bot.time.timeOfDay) + if (bot?.time?.timeOfDay) { + this.emitter.emit('time', bot.time.timeOfDay) + } this.emitterGotConnected() const [botX, botZ] = chunkPos(pos) diff --git a/src/appViewer.ts b/src/appViewer.ts index 5ac0beaf..d8149912 100644 --- a/src/appViewer.ts +++ b/src/appViewer.ts @@ -1,5 +1,5 @@ import { WorldDataEmitter, WorldDataEmitterWorker } from 'renderer/viewer/lib/worldDataEmitter' -import { PlayerStateRenderer } from 'renderer/viewer/lib/basePlayerState' +import { getInitialPlayerState, PlayerStateRenderer } from 'renderer/viewer/lib/basePlayerState' import { subscribeKey } from 'valtio/utils' import { defaultWorldRendererConfig, WorldRendererConfig } from 'renderer/viewer/lib/worldrendererCommon' import { Vec3 } from 'vec3' @@ -281,19 +281,31 @@ const initialMenuStart = async () => { if (appViewer.currentDisplay === 'world') { appViewer.resetBackend(true) } - appViewer.startPanorama() + const demo = new URLSearchParams(window.location.search).get('demo') + if (!demo) { + appViewer.startPanorama() + return + } // const version = '1.18.2' - // const version = '1.21.4' - // await appViewer.resourcesManager.loadMcData(version) - // const world = getSyncWorld(version) - // world.setBlockStateId(new Vec3(0, 64, 0), loadedData.blocksByName.water.defaultState) - // appViewer.resourcesManager.currentConfig = { version } - // await appViewer.resourcesManager.updateAssetsData({}) - // appViewer.playerState = new BasePlayerState() as any - // await appViewer.startWorld(world, 3) - // appViewer.backend?.updateCamera(new Vec3(0, 64, 2), 0, 0) - // void appViewer.worldView!.init(new Vec3(0, 64, 0)) + const version = '1.21.4' + const { loadMinecraftData } = await import('./connect') + const { getSyncWorld } = await import('../renderer/playground/shared') + await loadMinecraftData(version) + const world = getSyncWorld(version) + world.setBlockStateId(new Vec3(0, 64, 0), loadedData.blocksByName.water.defaultState) + world.setBlockStateId(new Vec3(1, 64, 0), loadedData.blocksByName.water.defaultState) + world.setBlockStateId(new Vec3(1, 64, 1), loadedData.blocksByName.water.defaultState) + world.setBlockStateId(new Vec3(0, 64, 1), loadedData.blocksByName.water.defaultState) + world.setBlockStateId(new Vec3(-1, 64, -1), loadedData.blocksByName.water.defaultState) + world.setBlockStateId(new Vec3(-1, 64, 0), loadedData.blocksByName.water.defaultState) + world.setBlockStateId(new Vec3(0, 64, -1), loadedData.blocksByName.water.defaultState) + appViewer.resourcesManager.currentConfig = { version } + appViewer.playerState.reactive = getInitialPlayerState() + await appViewer.resourcesManager.updateAssetsData({}) + await appViewer.startWorld(world, 3) + appViewer.backend!.updateCamera(new Vec3(0, 65.7, 0), 0, -Math.PI / 2) // Y+1 and pitch = PI/2 to look down + void appViewer.worldView!.init(new Vec3(0, 64, 0)) } window.initialMenuStart = initialMenuStart diff --git a/src/reactUi.tsx b/src/reactUi.tsx index 138696b0..8e94b35d 100644 --- a/src/reactUi.tsx +++ b/src/reactUi.tsx @@ -276,13 +276,17 @@ const PerComponentErrorBoundary = ({ children }) => { </ErrorBoundary>) } -renderToDom(<App />, { - strictMode: false, - selector: '#react-root', -}) +if (!new URLSearchParams(window.location.search).get('no-ui')) { + renderToDom(<App />, { + strictMode: false, + selector: '#react-root', + }) +} disableReactProfiling() function disableReactProfiling () { + if (window.reactPerfPatchApplied) return + window.reactPerfPatchApplied = true //@ts-expect-error window.performance.markOrig = window.performance.mark //@ts-expect-error From 5269ad21b5797133fcdd31a14674ad3449afba63 Mon Sep 17 00:00:00 2001 From: Max Lee <max@themoep.de> Date: Wed, 18 Jun 2025 14:07:00 +0000 Subject: [PATCH 034/181] feat: Add spectator mode entity spectating (#369) --- renderer/viewer/lib/basePlayerState.ts | 16 +++++-- renderer/viewer/lib/worldrendererCommon.ts | 14 +++--- renderer/viewer/three/cameraShake.ts | 9 ++++ renderer/viewer/three/entities.ts | 4 +- renderer/viewer/three/holdingBlock.ts | 16 +++---- renderer/viewer/three/panorama.ts | 2 +- renderer/viewer/three/world/cursorBlock.ts | 6 +-- renderer/viewer/three/worldrendererThree.ts | 33 ++++++++++--- src/appViewer.ts | 8 ++-- src/cameraRotationControls.ts | 2 +- src/controls.ts | 32 +++++++++++-- src/entities.ts | 51 ++++++++++++++++++--- src/inventoryWindows.ts | 2 +- src/mineflayer/playerState.ts | 8 ++-- 14 files changed, 150 insertions(+), 53 deletions(-) diff --git a/renderer/viewer/lib/basePlayerState.ts b/renderer/viewer/lib/basePlayerState.ts index fe6e39c4..26598cad 100644 --- a/renderer/viewer/lib/basePlayerState.ts +++ b/renderer/viewer/lib/basePlayerState.ts @@ -47,6 +47,17 @@ export const getInitialPlayerState = () => proxy({ shouldHideHand: false, heldItemMain: undefined as HandItemBlock | undefined, heldItemOff: undefined as HandItemBlock | undefined, + + cameraSpectatingEntity: undefined as number | undefined, +}) + +export const getPlayerStateUtils = (reactive: PlayerStateReactive) => ({ + isSpectator () { + return reactive.gameMode === 'spectator' + }, + isSpectatingEntity () { + return reactive.cameraSpectatingEntity !== undefined && reactive.gameMode === 'spectator' + } }) export const getInitialPlayerStateRenderer = () => ({ @@ -54,10 +65,9 @@ export const getInitialPlayerStateRenderer = () => ({ }) export type PlayerStateReactive = ReturnType<typeof getInitialPlayerState> +export type PlayerStateUtils = ReturnType<typeof getPlayerStateUtils> -export interface PlayerStateRenderer { - reactive: PlayerStateReactive -} +export type PlayerStateRenderer = PlayerStateReactive export const getItemSelector = (playerState: PlayerStateRenderer, specificProperties: ItemSpecificContextProperties, item?: import('prismarine-item').Item) => { return { diff --git a/renderer/viewer/lib/worldrendererCommon.ts b/renderer/viewer/lib/worldrendererCommon.ts index 1f0df0bd..78b69925 100644 --- a/renderer/viewer/lib/worldrendererCommon.ts +++ b/renderer/viewer/lib/worldrendererCommon.ts @@ -16,7 +16,7 @@ import { HighestBlockInfo, CustomBlockModels, BlockStateModelInfo, getBlockAsset import { chunkPos } from './simpleUtils' import { addNewStat, removeAllStats, updatePanesVisibility, updateStatText } from './ui/newStats' import { WorldDataEmitterWorker } from './worldDataEmitter' -import { PlayerStateRenderer } from './basePlayerState' +import { getPlayerStateUtils, PlayerStateReactive, PlayerStateRenderer, PlayerStateUtils } from './basePlayerState' import { MesherLogReader } from './mesherlogReader' import { setSkinsConfig } from './utils/skins' @@ -156,7 +156,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> abstract changeBackgroundColor (color: [number, number, number]): void worldRendererConfig: WorldRendererConfig - playerState: PlayerStateRenderer + playerStateReactive: PlayerStateReactive + playerStateUtils: PlayerStateUtils reactiveState: RendererReactiveState mesherLogReader: MesherLogReader | undefined forceCallFromMesherReplayer = false @@ -191,7 +192,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> constructor (public readonly resourcesManager: ResourcesManagerTransferred, public displayOptions: DisplayWorldOptions, public initOptions: GraphicsInitOptions) { this.snapshotInitialValues() this.worldRendererConfig = displayOptions.inWorldRenderingConfig - this.playerState = displayOptions.playerState + this.playerStateReactive = displayOptions.playerStateReactive + this.playerStateUtils = getPlayerStateUtils(this.playerStateReactive) this.reactiveState = displayOptions.rendererState // this.mesherLogReader = new MesherLogReader(this) this.renderUpdateEmitter.on('update', () => { @@ -304,11 +306,11 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> } } - onReactivePlayerStateUpdated<T extends keyof typeof this.displayOptions.playerState.reactive>(key: T, callback: (value: typeof this.displayOptions.playerState.reactive[T]) => void, initial = true) { + onReactivePlayerStateUpdated<T extends keyof PlayerStateReactive>(key: T, callback: (value: PlayerStateReactive[T]) => void, initial = true) { if (initial) { - callback(this.displayOptions.playerState.reactive[key]) + callback(this.playerStateReactive[key]) } - subscribeKey(this.displayOptions.playerState.reactive, key, callback) + subscribeKey(this.playerStateReactive, key, callback) } onReactiveConfigUpdated<T extends keyof typeof this.worldRendererConfig>(key: T, callback: (value: typeof this.worldRendererConfig[T]) => void) { diff --git a/renderer/viewer/three/cameraShake.ts b/renderer/viewer/three/cameraShake.ts index 6fe483cc..20f7143a 100644 --- a/renderer/viewer/three/cameraShake.ts +++ b/renderer/viewer/three/cameraShake.ts @@ -21,6 +21,10 @@ export class CameraShake { this.update() } + getBaseRotation () { + return { pitch: this.basePitch, yaw: this.baseYaw } + } + shakeFromDamage (yaw?: number) { // Add roll animation const startRoll = this.rollAngle @@ -35,6 +39,11 @@ export class CameraShake { } update () { + if (this.worldRenderer.playerStateUtils.isSpectatingEntity()) { + // Remove any shaking when spectating + this.rollAngle = 0 + this.rollAnimation = undefined + } // Update roll animation if (this.rollAnimation) { const now = performance.now() diff --git a/renderer/viewer/three/entities.ts b/renderer/viewer/three/entities.ts index 0e8e6384..6ddacc0b 100644 --- a/renderer/viewer/three/entities.ts +++ b/renderer/viewer/three/entities.ts @@ -870,9 +870,7 @@ export class Entities { const meta = getGeneralEntitiesMetadata(entity) - //@ts-expect-error - // set visibility - const isInvisible = entity.metadata?.[0] & 0x20 + const isInvisible = ((entity.metadata?.[0] ?? 0) as unknown as number) & 0x20 || (this.worldRenderer.playerStateReactive.cameraSpectatingEntity === entity.id && this.worldRenderer.playerStateUtils.isSpectator()) for (const child of mesh!.children ?? []) { if (child.name !== 'nametag') { child.visible = !isInvisible diff --git a/renderer/viewer/three/holdingBlock.ts b/renderer/viewer/three/holdingBlock.ts index 422076f0..af355f2e 100644 --- a/renderer/viewer/three/holdingBlock.ts +++ b/renderer/viewer/three/holdingBlock.ts @@ -116,12 +116,10 @@ export default class HoldingBlock { offHandModeLegacy = false swingAnimator: HandSwingAnimator | undefined - playerState: PlayerStateRenderer config: WorldRendererConfig constructor (public worldRenderer: WorldRendererThree, public offHand = false) { this.initCameraGroup() - this.playerState = worldRenderer.displayOptions.playerState this.worldRenderer.onReactivePlayerStateUpdated('heldItemMain', () => { if (!this.offHand) { this.updateItem() @@ -146,9 +144,9 @@ export default class HoldingBlock { // now watch over the player skin watchProperty( async () => { - return getMyHand(this.playerState.reactive.playerSkin, this.playerState.reactive.onlineMode ? this.playerState.reactive.username : undefined) + return getMyHand(this.worldRenderer.playerStateReactive.playerSkin, this.worldRenderer.playerStateReactive.onlineMode ? this.worldRenderer.playerStateReactive.username : undefined) }, - this.playerState.reactive, + this.worldRenderer.playerStateReactive, 'playerSkin', (newHand) => { if (newHand) { @@ -167,7 +165,7 @@ export default class HoldingBlock { updateItem () { if (!this.ready) return - const item = this.offHand ? this.playerState.reactive.heldItemOff : this.playerState.reactive.heldItemMain + const item = this.offHand ? this.worldRenderer.playerStateReactive.heldItemOff : this.worldRenderer.playerStateReactive.heldItemMain if (item) { void this.setNewItem(item) } else if (this.offHand) { @@ -357,8 +355,8 @@ export default class HoldingBlock { itemId: handItem.id, }, { 'minecraft:display_context': 'firstperson', - 'minecraft:use_duration': this.playerState.reactive.itemUsageTicks, - 'minecraft:using_item': !!this.playerState.reactive.itemUsageTicks, + 'minecraft:use_duration': this.worldRenderer.playerStateReactive.itemUsageTicks, + 'minecraft:using_item': !!this.worldRenderer.playerStateReactive.itemUsageTicks, }, this.lastItemModelName) if (result) { const { mesh: itemMesh, isBlock, modelName } = result @@ -475,7 +473,7 @@ export default class HoldingBlock { this.swingAnimator = new HandSwingAnimator(this.holdingBlockInnerGroup) this.swingAnimator.type = result.type if (this.config.viewBobbing) { - this.idleAnimator = new HandIdleAnimator(this.holdingBlockInnerGroup, this.playerState) + this.idleAnimator = new HandIdleAnimator(this.holdingBlockInnerGroup, this.worldRenderer.playerStateReactive) } } @@ -710,7 +708,7 @@ class HandIdleAnimator { // Check for state changes from player state if (this.playerState) { - const newState = this.playerState.reactive.movementState + const newState = this.playerState.movementState if (newState !== this.targetState) { this.setState(newState) } diff --git a/renderer/viewer/three/panorama.ts b/renderer/viewer/three/panorama.ts index 58a0fffa..7e975d4b 100644 --- a/renderer/viewer/three/panorama.ts +++ b/renderer/viewer/three/panorama.ts @@ -197,7 +197,7 @@ export class PanoramaRenderer { version, worldView, inWorldRenderingConfig: defaultWorldRendererConfig, - playerState: getInitialPlayerStateRenderer(), + playerStateReactive: getInitialPlayerStateRenderer().reactive, rendererState: getDefaultRendererState().reactive, nonReactiveState: getDefaultRendererState().nonReactive } diff --git a/renderer/viewer/three/world/cursorBlock.ts b/renderer/viewer/three/world/cursorBlock.ts index 495a2d55..b3be2ca2 100644 --- a/renderer/viewer/three/world/cursorBlock.ts +++ b/renderer/viewer/three/world/cursorBlock.ts @@ -61,16 +61,14 @@ export class CursorBlock { this.blockBreakMesh.name = 'blockBreakMesh' this.worldRenderer.scene.add(this.blockBreakMesh) - subscribeKey(this.worldRenderer.playerState.reactive, 'gameMode', () => { + this.worldRenderer.onReactivePlayerStateUpdated('gameMode', () => { this.updateLineMaterial() }) - - this.updateLineMaterial() } // Update functions updateLineMaterial () { - const inCreative = this.worldRenderer.displayOptions.playerState.reactive.gameMode === 'creative' + const inCreative = this.worldRenderer.playerStateReactive.gameMode === 'creative' const pixelRatio = this.worldRenderer.renderer.getPixelRatio() this.cursorLineMaterial = new LineMaterial({ diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index 78381a29..77100c5c 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -69,6 +69,9 @@ export class WorldRendererThree extends WorldRendererCommon { } fountains: Fountain[] = [] + private currentPosTween?: tweenJs.Tween<THREE.Vector3> + private currentRotTween?: tweenJs.Tween<{ pitch: number, yaw: number }> + get tilesRendered () { return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).tilesCount, 0) } @@ -150,7 +153,7 @@ export class WorldRendererThree extends WorldRendererCommon { override watchReactivePlayerState () { super.watchReactivePlayerState() this.onReactivePlayerStateUpdated('inWater', (value) => { - this.scene.fog = value ? new THREE.Fog(0x00_00_ff, 0.1, this.displayOptions.playerState.reactive.waterBreathing ? 100 : 20) : null + this.scene.fog = value ? new THREE.Fog(0x00_00_ff, 0.1, this.playerStateReactive.waterBreathing ? 100 : 20) : null }) this.onReactivePlayerStateUpdated('ambientLight', (value) => { if (!value) return @@ -238,7 +241,7 @@ export class WorldRendererThree extends WorldRendererCommon { } getItemRenderData (item: Record<string, any>, specificProps: ItemSpecificContextProperties) { - return getItemUv(item, specificProps, this.resourcesManager, this.playerState) + return getItemUv(item, specificProps, this.resourcesManager, this.playerStateReactive) } async demoModel () { @@ -430,7 +433,7 @@ export class WorldRendererThree extends WorldRendererCommon { } setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number) { - const yOffset = this.displayOptions.playerState.reactive.eyeHeight + const yOffset = this.playerStateReactive.eyeHeight this.updateCamera(pos?.offset(0, yOffset, 0) ?? null, yaw, pitch) this.media.tryIntersectMedia() @@ -448,10 +451,28 @@ export class WorldRendererThree extends WorldRendererCommon { pos.y -= this.camera.position.y // Fix Y position of camera in world } - new tweenJs.Tween(this.cameraObject.position).to({ x: pos.x, y: pos.y, z: pos.z }, 50).start() + this.currentPosTween?.stop() + this.currentPosTween = new tweenJs.Tween(this.cameraObject.position).to({ x: pos.x, y: pos.y, z: pos.z }, this.playerStateUtils.isSpectatingEntity() ? 150 : 50).start() // this.freeFlyState.position = pos } - this.cameraShake.setBaseRotation(pitch, yaw) + + if (this.playerStateUtils.isSpectatingEntity()) { + const rotation = this.cameraShake.getBaseRotation() + // wrap in the correct direction + let yawOffset = 0 + const halfPi = Math.PI / 2 + if (rotation.yaw < halfPi && yaw > Math.PI + halfPi) { + yawOffset = -Math.PI * 2 + } else if (yaw < halfPi && rotation.yaw > Math.PI + halfPi) { + yawOffset = Math.PI * 2 + } + this.currentRotTween?.stop() + this.currentRotTween = new tweenJs.Tween(rotation).to({ pitch, yaw: yaw + yawOffset }, 100) + .onUpdate(params => this.cameraShake.setBaseRotation(params.pitch, params.yaw - yawOffset)).start() + } else { + this.currentRotTween?.stop() + this.cameraShake.setBaseRotation(pitch, yaw) + } } debugChunksVisibilityOverride () { @@ -508,7 +529,7 @@ export class WorldRendererThree extends WorldRendererCommon { const cam = this.cameraGroupVr instanceof THREE.Group ? this.cameraGroupVr.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera this.renderer.render(this.scene, cam) - if (this.displayOptions.inWorldRenderingConfig.showHand && this.playerState.reactive.gameMode !== 'spectator' /* && !this.freeFlyMode */ && !this.renderer.xr.isPresenting) { + if (this.displayOptions.inWorldRenderingConfig.showHand && this.playerStateReactive.gameMode !== 'spectator' /* && !this.freeFlyMode */ && !this.renderer.xr.isPresenting) { this.holdingBlock.render(this.camera, this.renderer, this.ambientLight, this.directionalLight) this.holdingBlockLeft.render(this.camera, this.renderer, this.ambientLight, this.directionalLight) } diff --git a/src/appViewer.ts b/src/appViewer.ts index d8149912..0be6c2e8 100644 --- a/src/appViewer.ts +++ b/src/appViewer.ts @@ -1,5 +1,5 @@ import { WorldDataEmitter, WorldDataEmitterWorker } from 'renderer/viewer/lib/worldDataEmitter' -import { getInitialPlayerState, PlayerStateRenderer } from 'renderer/viewer/lib/basePlayerState' +import { getInitialPlayerState, PlayerStateRenderer, PlayerStateReactive } from 'renderer/viewer/lib/basePlayerState' import { subscribeKey } from 'valtio/utils' import { defaultWorldRendererConfig, WorldRendererConfig } from 'renderer/viewer/lib/worldrendererCommon' import { Vec3 } from 'vec3' @@ -67,7 +67,7 @@ export interface DisplayWorldOptions { version: string worldView: WorldDataEmitterWorker inWorldRenderingConfig: WorldRendererConfig - playerState: PlayerStateRenderer + playerStateReactive: PlayerStateReactive rendererState: RendererReactiveState nonReactiveState: NonReactiveState } @@ -180,7 +180,7 @@ export class AppViewer { this.worldView!.listenToBot(bot) } - async startWorld (world, renderDistance: number, playerStateSend: PlayerStateRenderer = this.playerState) { + async startWorld (world, renderDistance: number, playerStateSend: PlayerStateRenderer = this.playerState.reactive) { if (this.currentDisplay === 'world') throw new Error('World already started') this.currentDisplay = 'world' const startPosition = bot.entity?.position ?? new Vec3(0, 64, 0) @@ -192,7 +192,7 @@ export class AppViewer { version: this.resourcesManager.currentConfig!.version, worldView: this.worldView, inWorldRenderingConfig: this.inWorldRenderingConfig, - playerState: playerStateSend, + playerStateReactive: playerStateSend, rendererState: this.rendererState, nonReactiveState: this.nonReactiveState } diff --git a/src/cameraRotationControls.ts b/src/cameraRotationControls.ts index 849e5940..679a3a44 100644 --- a/src/cameraRotationControls.ts +++ b/src/cameraRotationControls.ts @@ -18,6 +18,7 @@ export function onCameraMove (e: MouseEvent | CameraMoveEvent) { if (!isGameActive(true)) return if (e.type === 'mousemove' && !document.pointerLockElement) return e.stopPropagation?.() + if (appViewer.playerState.utils.isSpectatingEntity()) return const now = performance.now() // todo: limit camera movement for now to avoid unexpected jumps if (now - lastMouseMove < 4 && !options.preciseMouseInput) return @@ -32,7 +33,6 @@ export function onCameraMove (e: MouseEvent | CameraMoveEvent) { updateMotion() } - export const moveCameraRawHandler = ({ x, y }: { x: number; y: number }) => { const maxPitch = 0.5 * Math.PI const minPitch = -0.5 * Math.PI diff --git a/src/controls.ts b/src/controls.ts index 94364f2d..d7ae758c 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -131,7 +131,14 @@ const setSprinting = (state: boolean) => { gameAdditionalState.isSprinting = state } +const isSpectatingEntity = () => { + return appViewer.playerState.utils.isSpectatingEntity() +} + contro.on('movementUpdate', ({ vector, soleVector, gamepadIndex }) => { + // Don't allow movement while spectating an entity + if (isSpectatingEntity()) return + if (gamepadIndex !== undefined && gamepadUiCursorState.display) { const deadzone = 0.1 // TODO make deadzone configurable if (Math.abs(soleVector.x) < deadzone && Math.abs(soleVector.z) < deadzone) { @@ -340,6 +347,9 @@ const cameraRotationControls = { cameraRotationControls.updateMovement() }, handleCommand (command: string, pressed: boolean) { + // Don't allow movement while spectating an entity + if (isSpectatingEntity()) return + const directionMap = { 'general.rotateCameraLeft': 'left', 'general.rotateCameraRight': 'right', @@ -361,6 +371,7 @@ window.cameraRotationControls = cameraRotationControls const setSneaking = (state: boolean) => { gameAdditionalState.isSneaking = state bot.setControlState('sneak', state) + } const onTriggerOrReleased = (command: Command, pressed: boolean) => { @@ -371,6 +382,7 @@ const onTriggerOrReleased = (command: Command, pressed: boolean) => { // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check switch (command) { case 'general.jump': + if (isSpectatingEntity()) break // if (viewer.world.freeFlyMode) { // const moveSpeed = 0.5 // viewer.world.freeFlyState.position.add(new Vec3(0, pressed ? moveSpeed : 0, 0)) @@ -545,10 +557,15 @@ contro.on('trigger', ({ command }) => { // no-op break case 'general.swapHands': { - bot._client.write('entity_action', { - entityId: bot.entity.id, - actionId: 6, - jumpBoost: 0 + if (isSpectatingEntity()) break + bot._client.write('block_dig', { + 'status': 6, + 'location': { + 'x': 0, + 'z': 0, + 'y': 0 + }, + 'face': 0, }) break } @@ -556,11 +573,13 @@ contro.on('trigger', ({ command }) => { // handled in onTriggerOrReleased break case 'general.inventory': + if (isSpectatingEntity()) break document.exitPointerLock?.() openPlayerInventory() break case 'general.drop': { - // if (bot.heldItem/* && ctrl */) bot.tossStack(bot.heldItem) + if (isSpectatingEntity()) break + // protocol 1.9+ bot._client.write('block_dig', { 'status': 4, 'location': { @@ -593,12 +612,15 @@ contro.on('trigger', ({ command }) => { showModal({ reactType: 'chat' }) break case 'general.selectItem': + if (isSpectatingEntity()) break void selectItem() break case 'general.nextHotbarSlot': + if (isSpectatingEntity()) break cycleHotbarSlot(1) break case 'general.prevHotbarSlot': + if (isSpectatingEntity()) break cycleHotbarSlot(-1) break case 'general.zoom': diff --git a/src/entities.ts b/src/entities.ts index 8ee36431..3ba4632a 100644 --- a/src/entities.ts +++ b/src/entities.ts @@ -43,7 +43,7 @@ customEvents.on('gameLoaded', () => { updateAutoJump() const playerPerAnimation = {} as Record<string, string> - const entityData = (e: Entity) => { + const checkEntityData = (e: Entity) => { if (!e.username) return window.debugEntityMetadata ??= {} window.debugEntityMetadata[e.username] = e @@ -96,28 +96,65 @@ customEvents.on('gameLoaded', () => { } }) + const updateCamera = (entity: Entity) => { + if (bot.game.gameMode !== 'spectator') return + bot.entity.position = entity.position.clone() + void bot.look(entity.yaw, entity.pitch, true) + bot.entity.yaw = entity.yaw + bot.entity.pitch = entity.pitch + } + bot.on('entityGone', (entity) => { bot.tracker.stopTrackingEntity(entity, true) }) bot.on('entityMoved', (e) => { - entityData(e) + checkEntityData(e) + if (appViewer.playerState.reactive.cameraSpectatingEntity === e.id) { + updateCamera(e) + } }) bot._client.on('entity_velocity', (packet) => { const e = bot.entities[packet.entityId] if (!e) return - entityData(e) + checkEntityData(e) }) for (const entity of Object.values(bot.entities)) { if (entity !== bot.entity) { - entityData(entity) + checkEntityData(entity) } } - bot.on('entitySpawn', entityData) - bot.on('entityUpdate', entityData) - bot.on('entityEquip', entityData) + bot.on('entitySpawn', (e) => { + checkEntityData(e) + if (appViewer.playerState.reactive.cameraSpectatingEntity === e.id) { + updateCamera(e) + } + }) + bot.on('entityUpdate', checkEntityData) + bot.on('entityEquip', checkEntityData) + + bot._client.on('camera', (packet) => { + if (bot.player.entity.id === packet.cameraId) { + if (appViewer.playerState.utils.isSpectatingEntity() && appViewer.playerState.reactive.cameraSpectatingEntity) { + const entity = bot.entities[appViewer.playerState.reactive.cameraSpectatingEntity] + appViewer.playerState.reactive.cameraSpectatingEntity = undefined + if (entity) { + // do a force entity update + bot.emit('entityUpdate', entity) + } + } + } else if (appViewer.playerState.reactive.gameMode === 'spectator') { + const entity = bot.entities[packet.cameraId] + appViewer.playerState.reactive.cameraSpectatingEntity = packet.cameraId + if (entity) { + updateCamera(entity) + // do a force entity update + bot.emit('entityUpdate', entity) + } + } + }) // Texture override from packet properties bot._client.on('player_info', (packet) => { diff --git a/src/inventoryWindows.ts b/src/inventoryWindows.ts index d03b9fa4..05426d15 100644 --- a/src/inventoryWindows.ts +++ b/src/inventoryWindows.ts @@ -217,7 +217,7 @@ const mapSlots = (slots: Array<RenderItem | Item | null>, isJei = false) => { try { if (slot.durabilityUsed && slot.maxDurability) slot.durabilityUsed = Math.min(slot.durabilityUsed, slot.maxDurability) const debugIsQuickbar = !isJei && i === bot.inventory.hotbarStart + bot.quickBarSlot - const modelName = getItemModelName(slot, { 'minecraft:display_context': 'gui', }, appViewer.resourcesManager, playerState) + const modelName = getItemModelName(slot, { 'minecraft:display_context': 'gui', }, appViewer.resourcesManager, appViewer.playerState.reactive) const slotCustomProps = renderSlot({ modelName, originalItemName: slot.name }, appViewer.resourcesManager, debugIsQuickbar) const itemCustomName = getItemName(slot) Object.assign(slot, { ...slotCustomProps, displayName: itemCustomName ?? slot.displayName }) diff --git a/src/mineflayer/playerState.ts b/src/mineflayer/playerState.ts index f80a6971..b7b4d2bd 100644 --- a/src/mineflayer/playerState.ts +++ b/src/mineflayer/playerState.ts @@ -1,5 +1,5 @@ import { HandItemBlock } from 'renderer/viewer/three/holdingBlock' -import { getInitialPlayerState, PlayerStateRenderer } from 'renderer/viewer/lib/basePlayerState' +import { getInitialPlayerState, getPlayerStateUtils, PlayerStateReactive, PlayerStateRenderer, PlayerStateUtils } from 'renderer/viewer/lib/basePlayerState' import { subscribe } from 'valtio' import { subscribeKey } from 'valtio/utils' import { gameAdditionalState } from '../globalState' @@ -8,7 +8,7 @@ import { gameAdditionalState } from '../globalState' * can be used only in main thread. Mainly for more convenient reactive state updates. * In renderer/ directory, use PlayerStateControllerRenderer type or worldRenderer.playerState. */ -export class PlayerStateControllerMain implements PlayerStateRenderer { +export class PlayerStateControllerMain { disableStateUpdates = false private timeOffGround = 0 @@ -18,7 +18,8 @@ export class PlayerStateControllerMain implements PlayerStateRenderer { private isUsingItem = false ready = false - reactive: PlayerStateRenderer['reactive'] + reactive: PlayerStateReactive + utils: PlayerStateUtils constructor () { customEvents.on('mineflayerBotCreated', () => { @@ -41,6 +42,7 @@ export class PlayerStateControllerMain implements PlayerStateRenderer { private botCreated () { console.log('bot created & plugins injected') this.reactive = getInitialPlayerState() + this.utils = getPlayerStateUtils(this.reactive) this.onBotCreatedOrGameJoined() const handleDimensionData = (data) => { From fdeb78d96b7b3ec3a5849169128395a5bfbe4f1a Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Fri, 20 Jun 2025 13:56:25 +0300 Subject: [PATCH 035/181] add /ping and /connect GET endpoints for server info/ping data --- pnpm-lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 60c25f84..09cbe69b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -151,7 +151,7 @@ importers: version: 2.0.4 net-browserify: specifier: github:zardoy/prismarinejs-net-browserify - version: https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/64e351867c5711de8a183464284a8eb7d77d5f39 + version: https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/17fb901e8ea480a52c8fd46373695be172be8aa5 node-gzip: specifier: ^1.1.2 version: 1.1.2 @@ -6900,8 +6900,8 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - net-browserify@https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/64e351867c5711de8a183464284a8eb7d77d5f39: - resolution: {tarball: https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/64e351867c5711de8a183464284a8eb7d77d5f39} + net-browserify@https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/17fb901e8ea480a52c8fd46373695be172be8aa5: + resolution: {tarball: https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/17fb901e8ea480a52c8fd46373695be172be8aa5} version: 0.2.4 nice-try@1.0.5: @@ -17773,7 +17773,7 @@ snapshots: neo-async@2.6.2: {} - net-browserify@https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/64e351867c5711de8a183464284a8eb7d77d5f39: + net-browserify@https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/17fb901e8ea480a52c8fd46373695be172be8aa5: dependencies: body-parser: 1.20.3 express: 4.21.2 From 7c8ccba2c1023fd6e7183ab6464cf32ba342ee4d Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Sat, 21 Jun 2025 03:34:04 +0300 Subject: [PATCH 036/181] add testIosCrash for debugging these scenarios --- src/appParams.ts | 1 + src/index.ts | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/appParams.ts b/src/appParams.ts index aec6fd0b..59a24788 100644 --- a/src/appParams.ts +++ b/src/appParams.ts @@ -45,6 +45,7 @@ export type AppQsParams = { onlyConnect?: string connectText?: string freezeSettings?: string + testIosCrash?: string // Replay params replayFilter?: string diff --git a/src/index.ts b/src/index.ts index 483abd74..9d104dbb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -831,6 +831,26 @@ export async function connect (connectOptions: ConnectOptions) { miscUiState.gameLoaded = true miscUiState.loadedServerIndex = connectOptions.serverIndex ?? '' customEvents.emit('gameLoaded') + + // Test iOS Safari crash by creating memory pressure + if (appQueryParams.testIosCrash) { + setTimeout(() => { + console.log('Starting iOS crash test with memory pressure...') + // eslint-disable-next-line sonarjs/no-unused-collection + const arrays: number[][] = [] + try { + // Create large arrays until we run out of memory + // eslint-disable-next-line no-constant-condition + while (true) { + const arr = Array.from({ length: 1024 * 1024 }).fill(0).map((_, i) => i) + arrays.push(arr) + } + } catch (e) { + console.error('Memory allocation failed:', e) + } + }, 1000) + } + progress.end() setLoadingScreenStatus(undefined) } catch (err) { From 5221104980feaf50260f7a4b70465f68be6d5302 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Sun, 22 Jun 2025 01:14:15 +0300 Subject: [PATCH 037/181] feat: F5: 3rd person view camera! --- renderer/viewer/lib/basePlayerState.ts | 7 +- renderer/viewer/lib/worldDataEmitter.ts | 14 +- renderer/viewer/lib/worldrendererCommon.ts | 5 + renderer/viewer/three/cameraShake.ts | 2 +- renderer/viewer/three/entities.ts | 238 +++++++++++++++----- renderer/viewer/three/world/vr.ts | 4 +- renderer/viewer/three/worldrendererThree.ts | 183 ++++++++++++++- src/controls.ts | 25 ++ src/entities.ts | 47 +++- 9 files changed, 457 insertions(+), 68 deletions(-) diff --git a/renderer/viewer/lib/basePlayerState.ts b/renderer/viewer/lib/basePlayerState.ts index 26598cad..f9e4b2c9 100644 --- a/renderer/viewer/lib/basePlayerState.ts +++ b/renderer/viewer/lib/basePlayerState.ts @@ -5,7 +5,7 @@ import type { HandItemBlock } from '../three/holdingBlock' export type MovementState = 'NOT_MOVING' | 'WALKING' | 'SPRINTING' | 'SNEAKING' export type ItemSpecificContextProperties = Partial<Pick<ItemSelector['properties'], 'minecraft:using_item' | 'minecraft:use_duration' | 'minecraft:use_cycle' | 'minecraft:display_context'>> - +export type CameraPerspective = 'first_person' | 'third_person_back' | 'third_person_front' export type BlockShape = { position: any; width: any; height: any; depth: any; } export type BlocksShapes = BlockShape[] @@ -47,6 +47,7 @@ export const getInitialPlayerState = () => proxy({ shouldHideHand: false, heldItemMain: undefined as HandItemBlock | undefined, heldItemOff: undefined as HandItemBlock | undefined, + perspective: 'first_person' as CameraPerspective, cameraSpectatingEntity: undefined as number | undefined, }) @@ -57,6 +58,10 @@ export const getPlayerStateUtils = (reactive: PlayerStateReactive) => ({ }, isSpectatingEntity () { return reactive.cameraSpectatingEntity !== undefined && reactive.gameMode === 'spectator' + }, + isThirdPerson () { + if ((this as PlayerStateUtils).isSpectatingEntity()) return false + return reactive.perspective === 'third_person_back' || reactive.perspective === 'third_person_front' } }) diff --git a/renderer/viewer/lib/worldDataEmitter.ts b/renderer/viewer/lib/worldDataEmitter.ts index 435d9183..78da96d9 100644 --- a/renderer/viewer/lib/worldDataEmitter.ts +++ b/renderer/viewer/lib/worldDataEmitter.ts @@ -18,6 +18,7 @@ export type WorldDataEmitterEvents = { blockUpdate: (data: { pos: Vec3, stateId: number }) => void entity: (data: any) => void entityMoved: (data: any) => void + playerEntity: (data: any) => void time: (data: number) => void renderDistance: (viewDistance: number) => void blockEntities: (data: Record<string, any> | { blockEntities: Record<string, any> }) => void @@ -90,7 +91,13 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo }) const emitEntity = (e, name = 'entity') => { - if (!e || e === bot.entity) return + if (!e) return + if (e === bot.entity) { + if (name === 'entity') { + this.emitter.emit('playerEntity', e) + } + return + } if (!e.name) return // mineflayer received update for not spawned entity e.objectData = entitiesObjectData.get(e.id) this.emitter.emit(name as any, { @@ -148,9 +155,11 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo // when dimension might change login: () => { void this.updatePosition(bot.entity.position, true) + this.emitter.emit('playerEntity', bot.entity) }, respawn: () => { void this.updatePosition(bot.entity.position, true) + this.emitter.emit('playerEntity', bot.entity) this.emitter.emit('onWorldSwitch') }, } satisfies Partial<BotEvents> @@ -200,6 +209,9 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo if (bot?.time?.timeOfDay) { this.emitter.emit('time', bot.time.timeOfDay) } + if (bot.entity) { + this.emitter.emit('playerEntity', bot.entity) + } this.emitterGotConnected() const [botX, botZ] = chunkPos(pos) diff --git a/renderer/viewer/lib/worldrendererCommon.ts b/renderer/viewer/lib/worldrendererCommon.ts index 78b69925..dfa4f43b 100644 --- a/renderer/viewer/lib/worldrendererCommon.ts +++ b/renderer/viewer/lib/worldrendererCommon.ts @@ -721,6 +721,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> updateEntity (e: any, isUpdate = false) { } + abstract updatePlayerEntity? (e: any): void + lightUpdate (chunkX: number, chunkZ: number) { } connect (worldView: WorldDataEmitterWorker) { @@ -732,6 +734,9 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> worldEmitter.on('entityMoved', (e) => { this.updateEntity(e, true) }) + worldEmitter.on('playerEntity', (e) => { + this.updatePlayerEntity?.(e) + }) let currentLoadChunkBatch = null as { timeout diff --git a/renderer/viewer/three/cameraShake.ts b/renderer/viewer/three/cameraShake.ts index 20f7143a..593b4628 100644 --- a/renderer/viewer/three/cameraShake.ts +++ b/renderer/viewer/three/cameraShake.ts @@ -72,7 +72,7 @@ export class CameraShake { } } - const camera = this.worldRenderer.cameraGroupVr || this.worldRenderer.camera + const camera = this.worldRenderer.cameraObject if (this.worldRenderer.cameraGroupVr) { // For VR camera, only apply yaw rotation diff --git a/renderer/viewer/three/entities.ts b/renderer/viewer/three/entities.ts index 6ddacc0b..b593fa7e 100644 --- a/renderer/viewer/three/entities.ts +++ b/renderer/viewer/three/entities.ts @@ -214,6 +214,7 @@ export type SceneEntity = THREE.Object3D & { export class Entities { entities = {} as Record<string, SceneEntity> + playerEntity: SceneEntity | null = null // Special entity for the player in third person entitiesOptions = { fontFamily: 'mojangles' } @@ -254,12 +255,49 @@ export class Entities { this.watchResourcesUpdates() } + handlePlayerEntity (playerData: SceneEntity['originalEntity']) { + // Create player entity if it doesn't exist + if (!this.playerEntity) { + // Create the player entity similar to how normal entities are created + const group = new THREE.Group() as unknown as SceneEntity + group.originalEntity = { ...playerData, name: 'player' } as SceneEntity['originalEntity'] + + const wrapper = new THREE.Group() + const playerObject = this.setupPlayerObject(playerData, wrapper, {}) + group.playerObject = playerObject + group.add(wrapper) + + group.name = 'player_entity' + this.playerEntity = group + this.worldRenderer.scene.add(group) + + void this.updatePlayerSkin(playerData.id, playerData.username, playerData.uuid ?? undefined, stevePngUrl) + } + + // Update position and rotation + if (playerData.position) { + this.playerEntity.position.set(playerData.position.x, playerData.position.y, playerData.position.z) + } + if (playerData.yaw !== undefined) { + this.playerEntity.rotation.y = playerData.yaw + } + + this.updateEntityEquipment(this.playerEntity, playerData) + } + clear () { for (const mesh of Object.values(this.entities)) { this.worldRenderer.scene.remove(mesh) disposeObject(mesh) } this.entities = {} + + // Clean up player entity + if (this.playerEntity) { + this.worldRenderer.scene.remove(this.playerEntity) + disposeObject(this.playerEntity) + this.playerEntity = null + } } reloadEntities () { @@ -309,8 +347,9 @@ export class Entities { const botPos = this.worldRenderer.viewerPosition const VISIBLE_DISTANCE = 10 * 10 - for (const entityId of Object.keys(this.entities)) { - const entity = this.entities[entityId] + // Update regular entities + for (const [entityId, entity] of [...Object.entries(this.entities), ['player_entity', this.playerEntity] as [string, SceneEntity | null]]) { + if (!entity) continue const { playerObject } = entity // Update animations @@ -318,9 +357,6 @@ export class Entities { playerObject.animation.update(playerObject, dt) } - // Update armor positions - this.syncArmorPositions(entity) - // Update visibility based on distance and chunk load status if (botPos && entity.position) { const dx = entity.position.x - botPos.x @@ -333,6 +369,32 @@ export class Entities { this.maybeRenderPlayerSkin(entityId) } + + if (entity.visible) { + // Update armor positions + this.syncArmorPositions(entity) + } + + if (entityId === 'player_entity') { + entity.visible = this.worldRenderer.playerStateUtils.isThirdPerson() + + if (entity.visible) { + // sync + const yOffset = this.worldRenderer.playerStateReactive.eyeHeight + const pos = this.worldRenderer.cameraObject.position.clone().add(new THREE.Vector3(0, -yOffset, 0)) + entity.position.set(pos.x, pos.y, pos.z) + + const rotation = this.worldRenderer.cameraShake.getBaseRotation() + entity.rotation.set(0, rotation.yaw, 0) + + // Sync head rotation + entity.traverse((c) => { + if (c.name === 'head') { + c.rotation.set(-rotation.pitch, 0, 0) + } + }) + } + } } } @@ -410,6 +472,7 @@ export class Entities { } getPlayerObject (entityId: string | number) { + if (this.playerEntity?.originalEntity.id === entityId) return this.playerEntity?.playerObject const playerObject = this.entities[entityId]?.playerObject return playerObject } @@ -568,21 +631,63 @@ export class Entities { } playAnimation (entityPlayerId, animation: 'walking' | 'running' | 'oneSwing' | 'idle' | 'crouch' | 'crouchWalking') { - const playerObject = this.getPlayerObject(entityPlayerId) - if (!playerObject) return + // TODO CLEANUP! + // Handle special player entity ID for bot entity in third person + if (entityPlayerId === 'player_entity' && this.playerEntity?.playerObject) { + const { playerObject } = this.playerEntity + if (animation === 'oneSwing') { + if (!(playerObject.animation instanceof WalkingGeneralSwing)) throw new Error('Expected WalkingGeneralSwing') + playerObject.animation.swingArm() + return + } - if (animation === 'oneSwing') { - if (!(playerObject.animation instanceof WalkingGeneralSwing)) throw new Error('Expected WalkingGeneralSwing') - playerObject.animation.swingArm() + if (playerObject.animation instanceof WalkingGeneralSwing) { + playerObject.animation.switchAnimationCallback = () => { + if (!(playerObject.animation instanceof WalkingGeneralSwing)) throw new Error('Expected WalkingGeneralSwing') + playerObject.animation.isMoving = animation === 'walking' || animation === 'running' || animation === 'crouchWalking' + playerObject.animation.isRunning = animation === 'running' + playerObject.animation.isCrouched = animation === 'crouch' || animation === 'crouchWalking' + } + } return } - if (playerObject.animation instanceof WalkingGeneralSwing) { - playerObject.animation.switchAnimationCallback = () => { + // Handle regular entities + const playerObject = this.getPlayerObject(entityPlayerId) + if (playerObject) { + if (animation === 'oneSwing') { if (!(playerObject.animation instanceof WalkingGeneralSwing)) throw new Error('Expected WalkingGeneralSwing') - playerObject.animation.isMoving = animation === 'walking' || animation === 'running' || animation === 'crouchWalking' - playerObject.animation.isRunning = animation === 'running' - playerObject.animation.isCrouched = animation === 'crouch' || animation === 'crouchWalking' + playerObject.animation.swingArm() + return + } + + if (playerObject.animation instanceof WalkingGeneralSwing) { + playerObject.animation.switchAnimationCallback = () => { + if (!(playerObject.animation instanceof WalkingGeneralSwing)) throw new Error('Expected WalkingGeneralSwing') + playerObject.animation.isMoving = animation === 'walking' || animation === 'running' || animation === 'crouchWalking' + playerObject.animation.isRunning = animation === 'running' + playerObject.animation.isCrouched = animation === 'crouch' || animation === 'crouchWalking' + } + } + return + } + + // Handle player entity (for third person view) - fallback for backwards compatibility + if (this.playerEntity?.playerObject) { + const { playerObject: playerEntityObject } = this.playerEntity + if (animation === 'oneSwing') { + if (!(playerEntityObject.animation instanceof WalkingGeneralSwing)) throw new Error('Expected WalkingGeneralSwing') + playerEntityObject.animation.swingArm() + return + } + + if (playerEntityObject.animation instanceof WalkingGeneralSwing) { + playerEntityObject.animation.switchAnimationCallback = () => { + if (!(playerEntityObject.animation instanceof WalkingGeneralSwing)) throw new Error('Expected WalkingGeneralSwing') + playerEntityObject.animation.isMoving = animation === 'walking' || animation === 'running' || animation === 'crouchWalking' + playerEntityObject.animation.isRunning = animation === 'running' + playerEntityObject.animation.isCrouched = animation === 'crouch' || animation === 'crouchWalking' + } } } } @@ -782,23 +887,10 @@ export class Entities { } } } else if (isPlayerModel) { - // CREATE NEW PLAYER ENTITY const wrapper = new THREE.Group() - const playerObject = new PlayerObject() as PlayerObjectType - playerObject.realPlayerUuid = entity.uuid ?? '' - playerObject.realUsername = entity.username ?? '' - playerObject.position.set(0, 16, 0) - - // fix issues with starfield - playerObject.traverse((obj) => { - if (obj instanceof THREE.Mesh && obj.material instanceof THREE.MeshStandardMaterial) { - obj.material.transparent = true - } - }) - //@ts-expect-error - wrapper.add(playerObject) - const scale = 1 / 16 - wrapper.scale.set(scale, scale, scale) + const playerObject = this.setupPlayerObject(entity, wrapper, overrides) + group.playerObject = playerObject + mesh = wrapper if (entity.username) { // todo proper colors @@ -811,13 +903,6 @@ export class Entities { //@ts-expect-error wrapper.add(nameTag) } - - group.playerObject = playerObject - wrapper.rotation.set(0, Math.PI, 0) - mesh = wrapper - playerObject.animation = new WalkingGeneralSwing() - //@ts-expect-error - playerObject.animation.isMoving = false } else { mesh = getEntityMesh(entity, this.worldRenderer, this.entitiesOptions, overrides) } @@ -857,16 +942,8 @@ export class Entities { mesh = e.children.find(c => c.name === 'mesh') } - // check if entity has armor - if (entity.equipment) { - const isPlayer = entity.type === 'player' - this.addItemModel(e, isPlayer ? 'right' : 'left', entity.equipment[0], isPlayer) - this.addItemModel(e, isPlayer ? 'left' : 'right', entity.equipment[1], isPlayer) - addArmorModel(this.worldRenderer, e, 'feet', entity.equipment[2]) - addArmorModel(this.worldRenderer, e, 'legs', entity.equipment[3], 2) - addArmorModel(this.worldRenderer, e, 'chest', entity.equipment[4]) - addArmorModel(this.worldRenderer, e, 'head', entity.equipment[5]) - } + // Update equipment + this.updateEntityEquipment(e, entity) const meta = getGeneralEntitiesMetadata(entity) @@ -1038,14 +1115,6 @@ export class Entities { e.username = entity.username } - if (entity.type === 'player' && entity.equipment && e.playerObject) { - const { playerObject } = e - playerObject.backEquipment = entity.equipment.some((item) => item?.name === 'elytra') ? 'elytra' : 'cape' - if (playerObject.cape.map === null) { - playerObject.cape.visible = false - } - } - this.updateEntityPosition(entity, justAdded, overrides) } @@ -1075,13 +1144,17 @@ export class Entities { loadedSkinEntityIds = new Set<string>() maybeRenderPlayerSkin (entityId: string) { - const mesh = this.entities[entityId] + let mesh = this.entities[entityId] + if (entityId === 'player_entity') { + mesh = this.playerEntity! + entityId = this.playerEntity?.originalEntity.id as any + } if (!mesh) return if (!mesh.playerObject) return if (!mesh.visible) return const MAX_DISTANCE_SKIN_LOAD = 128 - const cameraPos = this.worldRenderer.camera.position + const cameraPos = this.worldRenderer.cameraObject.position const distance = mesh.position.distanceTo(cameraPos) if (distance < MAX_DISTANCE_SKIN_LOAD && distance < (this.worldRenderer.viewDistance * 16)) { if (this.loadedSkinEntityIds.has(entityId)) return @@ -1234,13 +1307,60 @@ export class Entities { } } - raycastScene () { + raycastSceneDebug () { // return any object from scene. raycast from camera const raycaster = new THREE.Raycaster() raycaster.setFromCamera(new THREE.Vector2(0, 0), this.worldRenderer.camera) const intersects = raycaster.intersectObjects(this.worldRenderer.scene.children) return intersects[0]?.object } + + private setupPlayerObject (entity: SceneEntity['originalEntity'], wrapper: THREE.Group, overrides: { texture?: string }): PlayerObjectType { + const playerObject = new PlayerObject() as PlayerObjectType + playerObject.realPlayerUuid = entity.uuid ?? '' + playerObject.realUsername = entity.username ?? '' + playerObject.position.set(0, 16, 0) + + // fix issues with starfield + playerObject.traverse((obj) => { + if (obj instanceof THREE.Mesh && obj.material instanceof THREE.MeshStandardMaterial) { + obj.material.transparent = true + } + }) + + wrapper.add(playerObject as any) + const scale = 1 / 16 + wrapper.scale.set(scale, scale, scale) + wrapper.rotation.set(0, Math.PI, 0) + + // Set up animation + playerObject.animation = new WalkingGeneralSwing() + //@ts-expect-error + playerObject.animation.isMoving = false + + return playerObject + } + + private updateEntityEquipment (entityMesh: SceneEntity, entity: SceneEntity['originalEntity']) { + if (!entityMesh || !entity.equipment) return + + const isPlayer = entity.type === 'player' + this.addItemModel(entityMesh, isPlayer ? 'right' : 'left', entity.equipment[0], isPlayer) + this.addItemModel(entityMesh, isPlayer ? 'left' : 'right', entity.equipment[1], isPlayer) + addArmorModel(this.worldRenderer, entityMesh, 'feet', entity.equipment[2]) + addArmorModel(this.worldRenderer, entityMesh, 'legs', entity.equipment[3], 2) + addArmorModel(this.worldRenderer, entityMesh, 'chest', entity.equipment[4]) + addArmorModel(this.worldRenderer, entityMesh, 'head', entity.equipment[5]) + + // Update player-specific equipment + if (isPlayer && entityMesh.playerObject) { + const { playerObject } = entityMesh + playerObject.backEquipment = entity.equipment.some((item) => item?.name === 'elytra') ? 'elytra' : 'cape' + if (playerObject.cape.map === null) { + playerObject.cape.visible = false + } + } + } } function getGeneralEntitiesMetadata (entity: { name; metadata }): Partial<UnionToIntersection<EntityMetadataVersions[keyof EntityMetadataVersions]>> { diff --git a/renderer/viewer/three/world/vr.ts b/renderer/viewer/three/world/vr.ts index c2665585..ecf1b299 100644 --- a/renderer/viewer/three/world/vr.ts +++ b/renderer/viewer/three/world/vr.ts @@ -102,7 +102,7 @@ export async function initVR (worldRenderer: WorldRendererThree, documentRendere // hack for vr camera const user = new THREE.Group() - user.add(worldRenderer.camera) + user.name = 'vr-camera-container' worldRenderer.scene.add(user) const controllerModelFactory = new XRControllerModelFactory(new GLTFLoader()) const controller1 = renderer.xr.getControllerGrip(0) @@ -202,10 +202,12 @@ export async function initVR (worldRenderer: WorldRendererThree, documentRendere documentRenderer.frameRender(false) }) renderer.xr.addEventListener('sessionstart', () => { + user.add(worldRenderer.camera) worldRenderer.cameraGroupVr = user }) renderer.xr.addEventListener('sessionend', () => { worldRenderer.cameraGroupVr = undefined + user.remove(worldRenderer.camera) }) worldRenderer.abortController.signal.addEventListener('abort', disableVr) diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index 77100c5c..75cf473a 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -46,6 +46,7 @@ export class WorldRendererThree extends WorldRendererCommon { cursorBlock: CursorBlock onRender: Array<() => void> = [] cameraShake: CameraShake + cameraContainer: THREE.Object3D media: ThreeJsMedia waitingChunksToDisplay = {} as { [chunkKey: string]: SectionKey[] } camera: THREE.PerspectiveCamera @@ -68,6 +69,7 @@ export class WorldRendererThree extends WorldRendererCommon { } } fountains: Fountain[] = [] + DEBUG_RAYCAST = false private currentPosTween?: tweenJs.Tween<THREE.Vector3> private currentRotTween?: tweenJs.Tween<{ pitch: number, yaw: number }> @@ -109,7 +111,7 @@ export class WorldRendererThree extends WorldRendererCommon { } get cameraObject () { - return this.cameraGroupVr || this.camera + return this.cameraContainer } worldSwitchActions () { @@ -138,6 +140,10 @@ export class WorldRendererThree extends WorldRendererCommon { } } + updatePlayerEntity (e: any) { + this.entities.handlePlayerEntity(e) + } + resetScene () { this.scene.matrixAutoUpdate = false // for perf this.scene.background = new THREE.Color(this.initOptions.config.sceneBackground) @@ -148,6 +154,9 @@ export class WorldRendererThree extends WorldRendererCommon { const size = this.renderer.getSize(new THREE.Vector2()) this.camera = new THREE.PerspectiveCamera(75, size.x / size.y, 0.1, 1000) + this.cameraContainer = new THREE.Object3D() + this.cameraContainer.add(this.camera) + this.scene.add(this.cameraContainer) } override watchReactivePlayerState () { @@ -169,6 +178,12 @@ export class WorldRendererThree extends WorldRendererCommon { this.onReactivePlayerStateUpdated('diggingBlock', (value) => { this.cursorBlock.updateBreakAnimation(value ? { x: value.x, y: value.y, z: value.z } : undefined, value?.stage ?? null, value?.mergedShape) }) + this.onReactivePlayerStateUpdated('perspective', (value) => { + // Update camera perspective when it changes + const vecPos = new Vec3(this.cameraObject.position.x, this.cameraObject.position.y, this.cameraObject.position.z) + this.updateCamera(vecPos, this.cameraShake.getBaseRotation().yaw, this.cameraShake.getBaseRotation().pitch) + // todo also update camera when block within camera was changed + }) } override watchReactiveConfig () { @@ -439,6 +454,118 @@ export class WorldRendererThree extends WorldRendererCommon { this.media.tryIntersectMedia() } + getThirdPersonCamera (pos: THREE.Vector3 | null, yaw: number, pitch: number) { + pos ??= this.cameraObject.position + + // Calculate camera offset based on perspective + const isBack = this.playerStateReactive.perspective === 'third_person_back' + const distance = 4 // Default third person distance + + // Calculate direction vector using proper world orientation + // We need to get the camera's current look direction and use that for positioning + + // Create a direction vector that represents where the camera is looking + // This matches the Three.js camera coordinate system + const direction = new THREE.Vector3(0, 0, -1) // Forward direction in camera space + + // Apply the same rotation that's applied to the camera container + const pitchQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), pitch) + const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), yaw) + const finalQuat = new THREE.Quaternion().multiplyQuaternions(yawQuat, pitchQuat) + + // Transform the direction vector by the camera's rotation + direction.applyQuaternion(finalQuat) + + // For back view, we want the camera behind the player (opposite to view direction) + // For front view, we want the camera in front of the player (same as view direction) + if (isBack) { + direction.multiplyScalar(-1) + } + + // Create debug visualization if advanced stats are enabled + if (this.DEBUG_RAYCAST) { + this.debugRaycast(pos, direction, distance) + } + + // Perform raycast to avoid camera going through blocks + const raycaster = new THREE.Raycaster() + raycaster.set(pos, direction) + raycaster.far = distance // Limit raycast distance + + // Filter to only nearby chunks for performance + const nearbyChunks = Object.values(this.sectionObjects) + .filter(obj => obj.name === 'chunk' && obj.visible) + .filter(obj => { + // Get the mesh child which has the actual geometry + const mesh = obj.children.find(child => child.name === 'mesh') + if (!mesh) return false + + // Check distance from player position to chunk + const chunkWorldPos = new THREE.Vector3() + mesh.getWorldPosition(chunkWorldPos) + const distance = pos.distanceTo(chunkWorldPos) + return distance < 80 // Only check chunks within 80 blocks + }) + + // Get all mesh children for raycasting + const meshes: THREE.Object3D[] = [] + for (const chunk of nearbyChunks) { + const mesh = chunk.children.find(child => child.name === 'mesh') + if (mesh) meshes.push(mesh) + } + + const intersects = raycaster.intersectObjects(meshes, false) + + let finalDistance = distance + if (intersects.length > 0) { + // Use intersection distance minus a small offset to prevent clipping + finalDistance = Math.max(0.5, intersects[0].distance - 0.2) + } + + const finalPos = new Vec3( + pos.x + direction.x * finalDistance, + pos.y + direction.y * finalDistance, + pos.z + direction.z * finalDistance + ) + + return finalPos + } + + private debugRaycastHelper?: THREE.ArrowHelper + private debugHitPoint?: THREE.Mesh + + private debugRaycast (pos: THREE.Vector3, direction: THREE.Vector3, distance: number) { + // Remove existing debug objects + if (this.debugRaycastHelper) { + this.scene.remove(this.debugRaycastHelper) + this.debugRaycastHelper = undefined + } + if (this.debugHitPoint) { + this.scene.remove(this.debugHitPoint) + this.debugHitPoint = undefined + } + + // Create raycast arrow + this.debugRaycastHelper = new THREE.ArrowHelper( + direction.clone().normalize(), + pos, + distance, + 0xff_00_00, // Red color + distance * 0.1, + distance * 0.05 + ) + this.scene.add(this.debugRaycastHelper) + + // Create hit point indicator + const hitGeometry = new THREE.SphereGeometry(0.2, 8, 8) + const hitMaterial = new THREE.MeshBasicMaterial({ color: 0x00_ff_00 }) + this.debugHitPoint = new THREE.Mesh(hitGeometry, hitMaterial) + this.debugHitPoint.position.copy(pos).add(direction.clone().multiplyScalar(distance)) + this.scene.add(this.debugHitPoint) + } + + prevFramePerspective = null as string | null + updateCamera (pos: Vec3 | null, yaw: number, pitch: number): void { // if (this.freeFlyMode) { // pos = this.freeFlyState.position @@ -472,6 +599,42 @@ export class WorldRendererThree extends WorldRendererCommon { } else { this.currentRotTween?.stop() this.cameraShake.setBaseRotation(pitch, yaw) + + const { perspective } = this.playerStateReactive + if (perspective === 'third_person_back' || perspective === 'third_person_front') { + // Use getThirdPersonCamera for proper raycasting with max distance of 4 + const currentCameraPos = this.cameraObject.position + const thirdPersonPos = this.getThirdPersonCamera( + new THREE.Vector3(currentCameraPos.x, currentCameraPos.y, currentCameraPos.z), + yaw, + pitch + ) + + const distance = currentCameraPos.distanceTo(new THREE.Vector3(thirdPersonPos.x, thirdPersonPos.y, thirdPersonPos.z)) + // Apply Z offset based on perspective and calculated distance + const zOffset = perspective === 'third_person_back' ? distance : -distance + this.camera.position.set(0, 0, zOffset) + + if (perspective === 'third_person_front') { + // Flip camera view 180 degrees around Y axis for front view + this.camera.rotation.set(0, Math.PI, 0) + } else { + this.camera.rotation.set(0, 0, 0) + } + } else { + this.camera.position.set(0, 0, 0) + this.camera.rotation.set(0, 0, 0) + + // remove any debug raycasting + if (this.debugRaycastHelper) { + this.scene.remove(this.debugRaycastHelper) + this.debugRaycastHelper = undefined + } + if (this.debugHitPoint) { + this.scene.remove(this.debugHitPoint) + this.debugHitPoint = undefined + } + } } } @@ -529,7 +692,13 @@ export class WorldRendererThree extends WorldRendererCommon { const cam = this.cameraGroupVr instanceof THREE.Group ? this.cameraGroupVr.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera this.renderer.render(this.scene, cam) - if (this.displayOptions.inWorldRenderingConfig.showHand && this.playerStateReactive.gameMode !== 'spectator' /* && !this.freeFlyMode */ && !this.renderer.xr.isPresenting) { + if ( + this.displayOptions.inWorldRenderingConfig.showHand && + this.playerStateReactive.gameMode !== 'spectator' && + this.playerStateReactive.perspective === 'first_person' && + // !this.freeFlyMode && + !this.renderer.xr.isPresenting + ) { this.holdingBlock.render(this.camera, this.renderer, this.ambientLight, this.directionalLight) this.holdingBlockLeft.render(this.camera, this.renderer, this.ambientLight, this.directionalLight) } @@ -654,6 +823,16 @@ export class WorldRendererThree extends WorldRendererCommon { for (const mesh of Object.values(this.sectionObjects)) { this.scene.remove(mesh) } + + // Clean up debug objects + if (this.debugRaycastHelper) { + this.scene.remove(this.debugRaycastHelper) + this.debugRaycastHelper = undefined + } + if (this.debugHitPoint) { + this.scene.remove(this.debugHitPoint) + this.debugHitPoint = undefined + } } getLoadedChunksRelative (pos: Vec3, includeY = false) { diff --git a/src/controls.ts b/src/controls.ts index d7ae758c..44de8f11 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -29,6 +29,7 @@ import { appStorage } from './react/appStorageProvider' import { switchGameMode } from './packetsReplay/replayPackets' import { tabListState } from './react/PlayerListOverlayProvider' import { type ActionType, type ActionHoldConfig, type CustomAction } from './appConfig' +import { playerState } from './mineflayer/playerState' export const customKeymaps = proxy(appStorage.keybindings) subscribe(customKeymaps, () => { @@ -70,6 +71,7 @@ export const contro = new ControMax({ // client side zoom: ['KeyC'], viewerConsole: ['Backquote'], + togglePerspective: ['F5'], }, ui: { toggleFullscreen: ['F11'], @@ -439,6 +441,28 @@ const onTriggerOrReleased = (command: Command, pressed: boolean) => { case 'general.playersList': tabListState.isOpen = pressed break + case 'general.viewerConsole': + if (lastConnectOptions.value?.viewerWsConnect) { + showModal({ reactType: 'console' }) + } + break + case 'general.togglePerspective': + if (pressed) { + const currentPerspective = playerState.reactive.perspective + // eslint-disable-next-line sonarjs/no-nested-switch + switch (currentPerspective) { + case 'first_person': + playerState.reactive.perspective = 'third_person_back' + break + case 'third_person_back': + playerState.reactive.perspective = 'third_person_front' + break + case 'third_person_front': + playerState.reactive.perspective = 'first_person' + break + } + } + break } } else if (stringStartsWith(command, 'ui')) { switch (command) { @@ -554,6 +578,7 @@ contro.on('trigger', ({ command }) => { case 'general.debugOverlay': case 'general.debugOverlayHelpMenu': case 'general.playersList': + case 'general.togglePerspective': // no-op break case 'general.swapHands': { diff --git a/src/entities.ts b/src/entities.ts index 3ba4632a..b9f7998e 100644 --- a/src/entities.ts +++ b/src/entities.ts @@ -5,7 +5,7 @@ import { loader as autoJumpPlugin } from '@nxg-org/mineflayer-auto-jump' import { subscribeKey } from 'valtio/utils' import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods' import { options, watchValue } from './optionsStorage' -import { miscUiState } from './globalState' +import { gameAdditionalState, miscUiState } from './globalState' import { EntityStatus } from './mineflayer/entityStatus' @@ -52,6 +52,13 @@ customEvents.on('gameLoaded', () => { } } + const trackBotEntity = () => { + // Always track the bot entity for animations + if (bot.entity) { + bot.tracker.trackEntity(bot.entity) + } + } + let lastCall = 0 bot.on('physicsTick', () => { // throttle, tps: 6 @@ -64,7 +71,7 @@ customEvents.on('gameLoaded', () => { const speed = info.avgVel const WALKING_SPEED = 0.03 const SPRINTING_SPEED = 0.18 - const isCrouched = e['crouching'] + const isCrouched = e === bot.entity ? gameAdditionalState.isSneaking : e['crouching'] const isWalking = Math.abs(speed.x) > WALKING_SPEED || Math.abs(speed.z) > WALKING_SPEED const isSprinting = Math.abs(speed.x) > SPRINTING_SPEED || Math.abs(speed.z) > SPRINTING_SPEED @@ -73,7 +80,12 @@ customEvents.on('gameLoaded', () => { : isWalking ? (isSprinting ? 'running' : 'walking') : 'idle' if (newAnimation !== playerPerAnimation[id]) { - getThreeJsRendererMethods()?.playEntityAnimation(e.id, newAnimation) + // Handle bot entity animation specially (for player entity in third person) + if (e === bot.entity) { + getThreeJsRendererMethods()?.playEntityAnimation('player_entity', newAnimation) + } else { + getThreeJsRendererMethods()?.playEntityAnimation(e.id, newAnimation) + } playerPerAnimation[id] = newAnimation } } @@ -83,6 +95,25 @@ customEvents.on('gameLoaded', () => { getThreeJsRendererMethods()?.playEntityAnimation(e.id, 'oneSwing') }) + bot.on('botArmSwingStart', (hand) => { + if (hand === 'right') { + getThreeJsRendererMethods()?.playEntityAnimation('player_entity', 'oneSwing') + } + }) + + bot.inventory.on('updateSlot', (slot) => { + if (slot === 5 || slot === 6 || slot === 7 || slot === 8) { + const item = bot.inventory.slots[slot]! + bot.entity.equipment[slot - 3] = item + appViewer.worldView?.emit('playerEntity', bot.entity) + } + }) + bot.on('heldItemChanged', () => { + const item = bot.inventory.slots[bot.quickBarSlot + 36]! + bot.entity.equipment[0] = item + appViewer.worldView?.emit('playerEntity', bot.entity) + }) + bot._client.on('damage_event', (data) => { const { entityId, sourceTypeId: damage } = data getThreeJsRendererMethods()?.damageEntity(entityId, damage) @@ -126,6 +157,9 @@ customEvents.on('gameLoaded', () => { } } + // Track bot entity initially + trackBotEntity() + bot.on('entitySpawn', (e) => { checkEntityData(e) if (appViewer.playerState.reactive.cameraSpectatingEntity === e.id) { @@ -135,6 +169,13 @@ customEvents.on('gameLoaded', () => { bot.on('entityUpdate', checkEntityData) bot.on('entityEquip', checkEntityData) + // Re-track bot entity after login + bot.on('login', () => { + setTimeout(() => { + trackBotEntity() + }) // Small delay to ensure bot.entity is properly set + }) + bot._client.on('camera', (packet) => { if (bot.player.entity.id === packet.cameraId) { if (appViewer.playerState.utils.isSpectatingEntity() && appViewer.playerState.reactive.cameraSpectatingEntity) { From 32b19ab7af19e4f11821736874d0abbc87c62532 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Sun, 22 Jun 2025 01:20:34 +0300 Subject: [PATCH 038/181] fix: fix elytra skin --- renderer/viewer/three/entities.ts | 5 ++++- renderer/viewer/three/entity/armorModels.ts | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/renderer/viewer/three/entities.ts b/renderer/viewer/three/entities.ts index b593fa7e..fea7f710 100644 --- a/renderer/viewer/three/entities.ts +++ b/renderer/viewer/three/entities.ts @@ -25,7 +25,7 @@ import * as Entity from './entity/EntityMesh' import { getMesh } from './entity/EntityMesh' import { WalkingGeneralSwing } from './entity/animations' import { disposeObject } from './threeJsUtils' -import { armorModel, armorTextures } from './entity/armorModels' +import { armorModel, elytraTexture, armorTextures } from './entity/armorModels' import { WorldRendererThree } from './worldrendererThree' export const TWEEN_DURATION = 120 @@ -1356,6 +1356,9 @@ export class Entities { if (isPlayer && entityMesh.playerObject) { const { playerObject } = entityMesh playerObject.backEquipment = entity.equipment.some((item) => item?.name === 'elytra') ? 'elytra' : 'cape' + if (playerObject.backEquipment === 'elytra') { + void this.loadAndApplyCape(entity.id, elytraTexture) + } if (playerObject.cape.map === null) { playerObject.cape.visible = false } diff --git a/renderer/viewer/three/entity/armorModels.ts b/renderer/viewer/three/entity/armorModels.ts index 3a87f8db..3681344c 100644 --- a/renderer/viewer/three/entity/armorModels.ts +++ b/renderer/viewer/three/entity/armorModels.ts @@ -14,6 +14,7 @@ import { default as netheriteLayer1 } from 'mc-assets/dist/other-textures/latest import { default as netheriteLayer2 } from 'mc-assets/dist/other-textures/latest/models/armor/netherite_layer_2.png' import { default as turtleLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/turtle_layer_1.png' +export { default as elytraTexture } from 'mc-assets/dist/other-textures/latest/entity/elytra.png' export { default as armorModel } from './armorModels.json' export const armorTextures = { From 332bd4e0f354fb4e3472708b82b050671f3325d4 Mon Sep 17 00:00:00 2001 From: Vitaly <vital2580@icloud.com> Date: Sun, 22 Jun 2025 15:18:51 +0300 Subject: [PATCH 039/181] Display auth button --- src/react/AppStatusProvider.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/react/AppStatusProvider.tsx b/src/react/AppStatusProvider.tsx index 8bfed645..51c7991c 100644 --- a/src/react/AppStatusProvider.tsx +++ b/src/react/AppStatusProvider.tsx @@ -127,6 +127,7 @@ export default () => { }, []) const displayAuthButton = status.includes('This server appears to be an online server and you are providing no authentication.') + || JSON.stringify(minecraftJsonMessage ?? {}).toLowerCase().includes("authenticate") const hasVpnText = (text: string) => text.includes('VPN') || text.includes('Proxy') const displayVpnButton = hasVpnText(status) || (minecraftJsonMessage && hasVpnText(JSON.stringify(minecraftJsonMessage))) const authReconnectAction = async () => { From e9e144621f1419fa3dbd06204a7318054907a534 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Tue, 24 Jun 2025 02:13:06 +0300 Subject: [PATCH 040/181] improve auth features in edge cases --- src/index.ts | 3 ++- src/microsoftAuthflow.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 9d104dbb..185caab6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -209,10 +209,10 @@ export async function connect (connectOptions: ConnectOptions) { let ended = false let bot!: typeof __type_bot + let hadConnected = false const destroyAll = (wasKicked = false) => { if (ended) return loadingTimerState.loading = false - const hadConnected = !!bot if (!wasKicked && miscUiState.appConfig?.allowAutoConnect && appQueryParams.autoConnect && hadConnected) { location.reload() } @@ -856,6 +856,7 @@ export async function connect (connectOptions: ConnectOptions) { } catch (err) { handleError(err) } + hadConnected = true } // don't use spawn event, player can be dead bot.once(spawnEarlier ? 'forcedMove' : 'health', displayWorld) diff --git a/src/microsoftAuthflow.ts b/src/microsoftAuthflow.ts index 00f4e675..d759a7dc 100644 --- a/src/microsoftAuthflow.ts +++ b/src/microsoftAuthflow.ts @@ -71,7 +71,7 @@ export default async ({ tokenCaches, proxyBaseUrl, setProgressText = (text) => { onMsaCodeCallback(json) // this.codeCallback(json) } - if (json.error) throw new Error(json.error) + if (json.error) throw new Error(`Auth server error: ${json.error}`) if (json.token) result = json if (json.newCache) setCacheResult(json.newCache) } From 383e6c4d80dff331550be8d2893b5d0fdd3a729a Mon Sep 17 00:00:00 2001 From: Vitaly <vital2580@icloud.com> Date: Tue, 24 Jun 2025 10:03:23 +0000 Subject: [PATCH 041/181] fix lint --- src/react/AppStatusProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/AppStatusProvider.tsx b/src/react/AppStatusProvider.tsx index 51c7991c..e7e36cb7 100644 --- a/src/react/AppStatusProvider.tsx +++ b/src/react/AppStatusProvider.tsx @@ -127,7 +127,7 @@ export default () => { }, []) const displayAuthButton = status.includes('This server appears to be an online server and you are providing no authentication.') - || JSON.stringify(minecraftJsonMessage ?? {}).toLowerCase().includes("authenticate") + || JSON.stringify(minecraftJsonMessage ?? {}).toLowerCase().includes('authenticate') const hasVpnText = (text: string) => text.includes('VPN') || text.includes('Proxy') const displayVpnButton = hasVpnText(status) || (minecraftJsonMessage && hasVpnText(JSON.stringify(minecraftJsonMessage))) const authReconnectAction = async () => { From 1148378ce6d464b16bb9d1391e5a0aee770caa9f Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Wed, 25 Jun 2025 15:11:18 +0300 Subject: [PATCH 042/181] fix vr again --- renderer/viewer/three/worldrendererThree.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index 75cf473a..b4ae4961 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -111,7 +111,7 @@ export class WorldRendererThree extends WorldRendererCommon { } get cameraObject () { - return this.cameraContainer + return this.cameraGroupVr ?? this.cameraContainer } worldSwitchActions () { From 2055579b729128ccba94b3decd54fa67d436dbe9 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Wed, 25 Jun 2025 15:22:07 +0300 Subject: [PATCH 043/181] feat: finally add block RESCALE support! Cobwebs and ascending rails are now rendered correctly --- renderer/viewer/lib/mesher/models.ts | 33 +++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/renderer/viewer/lib/mesher/models.ts b/renderer/viewer/lib/mesher/models.ts index c2075764..a69d3e9a 100644 --- a/renderer/viewer/lib/mesher/models.ts +++ b/renderer/viewer/lib/mesher/models.ts @@ -336,7 +336,7 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO: let localShift = null as any if (element.rotation && !needTiles) { - // todo do we support rescale? + // Rescale support for block model rotations localMatrix = buildRotationMatrix( element.rotation.axis, element.rotation.angle @@ -349,6 +349,37 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO: element.rotation.origin ) ) + + // Apply rescale if specified + if (element.rotation.rescale) { + const FIT_TO_BLOCK_SCALE_MULTIPLIER = 2 - Math.sqrt(2) + const angleRad = element.rotation.angle * Math.PI / 180 + const scale = Math.abs(Math.sin(angleRad)) * FIT_TO_BLOCK_SCALE_MULTIPLIER + + // Get axis vector components (1 for the rotation axis, 0 for others) + const axisX = element.rotation.axis === 'x' ? 1 : 0 + const axisY = element.rotation.axis === 'y' ? 1 : 0 + const axisZ = element.rotation.axis === 'z' ? 1 : 0 + + // Create scale matrix: scale = (1 - axisComponent) * scaleFactor + 1 + const scaleMatrix = [ + [(1 - axisX) * scale + 1, 0, 0], + [0, (1 - axisY) * scale + 1, 0], + [0, 0, (1 - axisZ) * scale + 1] + ] + + // Apply scaling to the transformation matrix + localMatrix = matmulmat3(localMatrix, scaleMatrix) + + // Recalculate shift with the new matrix + localShift = vecsub3( + element.rotation.origin, + matmul3( + localMatrix, + element.rotation.origin + ) + ) + } } const aos: number[] = [] From 3a7f267b5bed997b55d6a9268d984e7b72c262f0 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Thu, 26 Jun 2025 04:34:08 +0300 Subject: [PATCH 044/181] dont ignore patch failures --- package.json | 4 +++- src/mineflayer/websocket-core.ts | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 3559169d..25cce1a5 100644 --- a/package.json +++ b/package.json @@ -227,7 +227,9 @@ "cypress", "esbuild", "fsevents" - ] + ], + "ignorePatchFailures": false, + "allowUnusedPatches": false }, "packageManager": "pnpm@10.8.0+sha512.0e82714d1b5b43c74610193cb20734897c1d00de89d0e18420aebc5977fa13d780a9cb05734624e81ebd81cc876cd464794850641c48b9544326b5622ca29971" } diff --git a/src/mineflayer/websocket-core.ts b/src/mineflayer/websocket-core.ts index d24bd6be..0edd2497 100644 --- a/src/mineflayer/websocket-core.ts +++ b/src/mineflayer/websocket-core.ts @@ -22,6 +22,8 @@ export const getWebsocketStream = async (host: string) => { ws.send(data) }) + clientDuplex.on('error', () => {}) + ws.addEventListener('message', async message => { let { data } = message if (data instanceof Blob) { From b839bb8b9baa4111400471cc74c96cc929aa21cc Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Thu, 26 Jun 2025 06:01:13 +0300 Subject: [PATCH 045/181] rm readme patching --- patches/minecraft-protocol.patch | 3 --- pnpm-lock.yaml | 16 ++++++++-------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/patches/minecraft-protocol.patch b/patches/minecraft-protocol.patch index efc79176..a3a4a6a3 100644 --- a/patches/minecraft-protocol.patch +++ b/patches/minecraft-protocol.patch @@ -1,6 +1,3 @@ -diff --git a/README.md b/README.md -deleted file mode 100644 -index fbcaa43667323a58b8110a4495938c2c6d2d6f83..0000000000000000000000000000000000000000 diff --git a/src/client/chat.js b/src/client/chat.js index f14269bea055d4329cd729271e7406ec4b344de7..00f5482eb6e3c911381ca9a728b1b4aae0d1d337 100644 --- a/src/client/chat.js diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 09cbe69b..fbdf89ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,7 +22,7 @@ overrides: patchedDependencies: minecraft-protocol: - hash: 1546deaf50efae3d6564fcc9f08da99d3ae8096ac98f420b87b48b8c9501fd37 + hash: 09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762 path: patches/minecraft-protocol.patch mineflayer-item-map-downloader@1.2.0: hash: a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad @@ -142,7 +142,7 @@ importers: version: 3.89.0 minecraft-protocol: specifier: github:PrismarineJS/node-minecraft-protocol#master - version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=1546deaf50efae3d6564fcc9f08da99d3ae8096ac98f420b87b48b8c9501fd37)(encoding@0.1.13) + version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) mineflayer-item-map-downloader: specifier: github:zardoy/mineflayer-item-map-downloader version: https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad)(encoding@0.1.13) @@ -13224,7 +13224,7 @@ snapshots: flatmap: 0.0.3 long: 5.3.1 minecraft-data: 3.89.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=1546deaf50efae3d6564fcc9f08da99d3ae8096ac98f420b87b48b8c9501fd37)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 @@ -13260,7 +13260,7 @@ snapshots: flatmap: 0.0.3 long: 5.3.1 minecraft-data: 3.89.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=1546deaf50efae3d6564fcc9f08da99d3ae8096ac98f420b87b48b8c9501fd37)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 @@ -17131,7 +17131,7 @@ snapshots: dependencies: '@zardoy/flying-squid': 0.0.49(encoding@0.1.13) exit-hook: 2.2.1 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=1546deaf50efae3d6564fcc9f08da99d3ae8096ac98f420b87b48b8c9501fd37)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/45f29f9fd1ad6d43d76bbd0b196f2ed4e675772b(encoding@0.1.13) prismarine-item: 1.16.0 ws: 8.18.1 @@ -17450,7 +17450,7 @@ snapshots: - '@types/react' - react - minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=1546deaf50efae3d6564fcc9f08da99d3ae8096ac98f420b87b48b8c9501fd37)(encoding@0.1.13): + minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13): dependencies: '@types/node-rsa': 1.1.4 '@types/readable-stream': 4.0.18 @@ -17549,7 +17549,7 @@ snapshots: mineflayer@4.27.0(encoding@0.1.13): dependencies: minecraft-data: 3.89.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=1546deaf50efae3d6564fcc9f08da99d3ae8096ac98f420b87b48b8c9501fd37)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) prismarine-biome: 1.3.0(minecraft-data@3.89.0)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chat: 1.11.0 @@ -17573,7 +17573,7 @@ snapshots: dependencies: '@nxg-org/mineflayer-physics-util': 1.8.10 minecraft-data: 3.89.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=1546deaf50efae3d6564fcc9f08da99d3ae8096ac98f420b87b48b8c9501fd37)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) prismarine-biome: 1.3.0(minecraft-data@3.89.0)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chat: 1.11.0 From 0b1bc76327b7834ab015d0b605e752def00d6cfa Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Thu, 26 Jun 2025 06:22:38 +0300 Subject: [PATCH 046/181] fix ws --- src/mineflayer/minecraft-protocol-extra.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mineflayer/minecraft-protocol-extra.ts b/src/mineflayer/minecraft-protocol-extra.ts index f3cf11e3..65260979 100644 --- a/src/mineflayer/minecraft-protocol-extra.ts +++ b/src/mineflayer/minecraft-protocol-extra.ts @@ -31,7 +31,9 @@ export const pingServerVersion = async (ip: string, port?: number, mergeOptions: }) if (mergeOptions.stream) { mergeOptions.stream.on('end', (err) => { - reject(new Error('Connection closed')) + setTimeout(() => { + reject(new Error('Connection closed. Please report if you see this but the server is actually fine.')) + }) }) } }) From eedd9f1b8f11ee21797d23f87aebbf591e95f29d Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Fri, 27 Jun 2025 16:28:15 +0300 Subject: [PATCH 047/181] feat: Now settings and servers list synced via top-domain cookies! Eg different subdomains like s.mcraft.fun and mcraft.fun will now share the same settings! Can be disabled. feat: Now its possible to import data! --- rsbuild.config.ts | 1 + src/core/importExport.ts | 219 +++++++++++++++++++++ src/defaultOptions.ts | 152 +++++++++++++++ src/globalState.ts | 2 + src/optionsGuiScheme.tsx | 76 +++----- src/optionsStorage.ts | 165 +--------------- src/react/StorageConflictModal.tsx | 94 +++++++++ src/react/appStorageProvider.ts | 294 +++++++++++++++++++++++++++-- src/reactUi.tsx | 2 + 9 files changed, 780 insertions(+), 225 deletions(-) create mode 100644 src/core/importExport.ts create mode 100644 src/defaultOptions.ts create mode 100644 src/react/StorageConflictModal.tsx diff --git a/rsbuild.config.ts b/rsbuild.config.ts index 5e76646e..548be4e2 100644 --- a/rsbuild.config.ts +++ b/rsbuild.config.ts @@ -140,6 +140,7 @@ const appConfig = defineConfig({ 'process.env.RELEASE_CHANGELOG': JSON.stringify(releaseChangelog), 'process.env.DISABLE_SERVICE_WORKER': JSON.stringify(disableServiceWorker), 'process.env.INLINED_APP_CONFIG': JSON.stringify(configSource === 'BUNDLED' ? configJson : null), + 'process.env.ENABLE_COOKIE_STORAGE': JSON.stringify(process.env.ENABLE_COOKIE_STORAGE || true), }, }, server: { diff --git a/src/core/importExport.ts b/src/core/importExport.ts new file mode 100644 index 00000000..b3e26347 --- /dev/null +++ b/src/core/importExport.ts @@ -0,0 +1,219 @@ +import { appStorage } from '../react/appStorageProvider' +import { getChangedSettings, options } from '../optionsStorage' +import { customKeymaps } from '../controls' +import { showInputsModal } from '../react/SelectOption' + +interface ExportedFile { + _about: string + options?: Record<string, any> + keybindings?: Record<string, any> + servers?: any[] + username?: string + proxy?: string + proxies?: string[] + accountTokens?: any[] +} + +export const importData = async () => { + try { + const input = document.createElement('input') + input.type = 'file' + input.accept = '.json' + input.click() + + const file = await new Promise<File>((resolve) => { + input.onchange = () => { + if (!input.files?.[0]) return + resolve(input.files[0]) + } + }) + + const text = await file.text() + const data = JSON.parse(text) + + if (!data._about?.includes('Minecraft Web Client')) { + const doContinue = confirm('This file does not appear to be a Minecraft Web Client profile. Continue anyway?') + if (!doContinue) return + } + + // Build available data types for selection + const availableData: Record<keyof Omit<ExportedFile, '_about'>, { present: boolean, description: string }> = { + options: { present: !!data.options, description: 'Game settings and preferences' }, + keybindings: { present: !!data.keybindings, description: 'Custom key mappings' }, + servers: { present: !!data.servers, description: 'Saved server list' }, + username: { present: !!data.username, description: 'Username' }, + proxy: { present: !!data.proxy, description: 'Selected proxy server' }, + proxies: { present: !!data.proxies, description: 'Global proxies list' }, + accountTokens: { present: !!data.accountTokens, description: 'Account authentication tokens' }, + } + + // Filter to only present data types + const presentTypes = Object.fromEntries(Object.entries(availableData) + .filter(([_, info]) => info.present) + .map<any>(([key, info]) => [key, info])) + + if (Object.keys(presentTypes).length === 0) { + alert('No compatible data found in the imported file.') + return + } + + const importChoices = await showInputsModal('Select Data to Import', { + mergeData: { + type: 'checkbox', + label: 'Merge with existing data (uncheck to remove old data)', + defaultValue: true, + }, + ...Object.fromEntries(Object.entries(presentTypes).map(([key, info]) => [key, { + type: 'checkbox', + label: info.description, + defaultValue: true, + }])) + }) as { mergeData: boolean } & Record<keyof ExportedFile, boolean> + + if (!importChoices) return + + const importedTypes: string[] = [] + const shouldMerge = importChoices.mergeData + + if (importChoices.options && data.options) { + if (shouldMerge) { + Object.assign(options, data.options) + } else { + for (const key of Object.keys(options)) { + if (key in data.options) { + options[key as any] = data.options[key] + } + } + } + importedTypes.push('settings') + } + + if (importChoices.keybindings && data.keybindings) { + if (shouldMerge) { + Object.assign(customKeymaps, data.keybindings) + } else { + for (const key of Object.keys(customKeymaps)) delete customKeymaps[key] + Object.assign(customKeymaps, data.keybindings) + } + importedTypes.push('keybindings') + } + + if (importChoices.servers && data.servers) { + if (shouldMerge && appStorage.serversList) { + // Merge by IP, update existing entries and add new ones + const existingIps = new Set(appStorage.serversList.map(s => s.ip)) + const newServers = data.servers.filter(s => !existingIps.has(s.ip)) + appStorage.serversList = [...appStorage.serversList, ...newServers] + } else { + appStorage.serversList = data.servers + } + importedTypes.push('servers') + } + + if (importChoices.username && data.username) { + appStorage.username = data.username + importedTypes.push('username') + } + + if ((importChoices.proxy && data.proxy) || (importChoices.proxies && data.proxies)) { + if (!appStorage.proxiesData) { + appStorage.proxiesData = { proxies: [], selected: '' } + } + + if (importChoices.proxies && data.proxies) { + if (shouldMerge) { + // Merge unique proxies + const uniqueProxies = new Set([...appStorage.proxiesData.proxies, ...data.proxies]) + appStorage.proxiesData.proxies = [...uniqueProxies] + } else { + appStorage.proxiesData.proxies = data.proxies + } + importedTypes.push('proxies list') + } + + if (importChoices.proxy && data.proxy) { + appStorage.proxiesData.selected = data.proxy + importedTypes.push('selected proxy') + } + } + + if (importChoices.accountTokens && data.accountTokens) { + if (shouldMerge && appStorage.authenticatedAccounts) { + // Merge by unique identifier (assuming accounts have some unique ID or username) + const existingAccounts = new Set(appStorage.authenticatedAccounts.map(a => a.username)) + const newAccounts = data.accountTokens.filter(a => !existingAccounts.has(a.username)) + appStorage.authenticatedAccounts = [...appStorage.authenticatedAccounts, ...newAccounts] + } else { + appStorage.authenticatedAccounts = data.accountTokens + } + importedTypes.push('account tokens') + } + + alert(`Profile imported successfully! Imported data: ${importedTypes.join(', ')}.\nYou may need to reload the page for some changes to take effect.`) + } catch (err) { + console.error('Failed to import profile:', err) + alert('Failed to import profile: ' + (err.message || err)) + } +} + +export const exportData = async () => { + const data = await showInputsModal('Export Profile', { + profileName: { + type: 'text', + }, + exportSettings: { + type: 'checkbox', + defaultValue: true, + }, + exportKeybindings: { + type: 'checkbox', + defaultValue: true, + }, + exportServers: { + type: 'checkbox', + defaultValue: true, + }, + saveUsernameAndProxy: { + type: 'checkbox', + defaultValue: true, + }, + exportGlobalProxiesList: { + type: 'checkbox', + defaultValue: false, + }, + exportAccountTokens: { + type: 'checkbox', + defaultValue: false, + }, + }) + const fileName = `${data.profileName ? `${data.profileName}-` : ''}web-client-profile.json` + const json: ExportedFile = { + _about: 'Minecraft Web Client (mcraft.fun) Profile', + ...data.exportSettings ? { + options: getChangedSettings(), + } : {}, + ...data.exportKeybindings ? { + keybindings: customKeymaps, + } : {}, + ...data.exportServers ? { + servers: appStorage.serversList, + } : {}, + ...data.saveUsernameAndProxy ? { + username: appStorage.username, + proxy: appStorage.proxiesData?.selected, + } : {}, + ...data.exportGlobalProxiesList ? { + proxies: appStorage.proxiesData?.proxies, + } : {}, + ...data.exportAccountTokens ? { + accountTokens: appStorage.authenticatedAccounts, + } : {}, + } + const blob = new Blob([JSON.stringify(json, null, 2)], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = fileName + a.click() + URL.revokeObjectURL(url) +} diff --git a/src/defaultOptions.ts b/src/defaultOptions.ts new file mode 100644 index 00000000..d2d510ec --- /dev/null +++ b/src/defaultOptions.ts @@ -0,0 +1,152 @@ +export const defaultOptions = { + renderDistance: 3, + keepChunksDistance: 1, + multiplayerRenderDistance: 3, + closeConfirmation: true, + autoFullScreen: false, + mouseRawInput: true, + autoExitFullscreen: false, + localUsername: 'wanderer', + mouseSensX: 50, + mouseSensY: -1, + chatWidth: 320, + chatHeight: 180, + chatScale: 100, + chatOpacity: 100, + chatOpacityOpened: 100, + messagesLimit: 200, + volume: 50, + enableMusic: false, + // fov: 70, + fov: 75, + guiScale: 3, + autoRequestCompletions: true, + touchButtonsSize: 40, + touchButtonsOpacity: 80, + touchButtonsPosition: 12, + touchControlsPositions: getDefaultTouchControlsPositions(), + touchControlsSize: getTouchControlsSize(), + touchMovementType: 'modern' as 'modern' | 'classic', + touchInteractionType: 'classic' as 'classic' | 'buttons', + gpuPreference: 'default' as 'default' | 'high-performance' | 'low-power', + backgroundRendering: '20fps' as 'full' | '20fps' | '5fps', + /** @unstable */ + disableAssets: false, + /** @unstable */ + debugLogNotFrequentPackets: false, + unimplementedContainers: false, + dayCycleAndLighting: true, + loadPlayerSkins: true, + renderEars: true, + lowMemoryMode: false, + starfieldRendering: true, + enabledResourcepack: null as string | null, + useVersionsTextures: 'latest', + serverResourcePacks: 'prompt' as 'prompt' | 'always' | 'never', + showHand: true, + viewBobbing: true, + displayRecordButton: true, + packetsLoggerPreset: 'all' as 'all' | 'no-buffers', + serversAutoVersionSelect: 'auto' as 'auto' | 'latest' | '1.20.4' | string, + customChannels: false, + remoteContentNotSameOrigin: false as boolean | string[], + packetsRecordingAutoStart: false, + language: 'auto', + preciseMouseInput: false, + // todo ui setting, maybe enable by default? + waitForChunksRender: false as 'sp-only' | boolean, + jeiEnabled: true as boolean | Array<'creative' | 'survival' | 'adventure' | 'spectator'>, + modsSupport: false, + modsAutoUpdate: 'check' as 'check' | 'never' | 'always', + modsUpdatePeriodCheck: 24, // hours + preventBackgroundTimeoutKick: false, + preventSleep: false, + debugContro: false, + debugChatScroll: false, + chatVanillaRestrictions: true, + debugResponseTimeIndicator: false, + chatPingExtension: true, + // antiAliasing: false, + topRightTimeDisplay: 'only-fullscreen' as 'only-fullscreen' | 'always' | 'never', + + clipWorldBelowY: undefined as undefined | number, // will be removed + disableSignsMapsSupport: false, + singleplayerAutoSave: false, + showChunkBorders: false, // todo rename option + frameLimit: false as number | false, + alwaysBackupWorldBeforeLoading: undefined as boolean | undefined | null, + alwaysShowMobileControls: false, + excludeCommunicationDebugEvents: [], + preventDevReloadWhilePlaying: false, + numWorkers: 4, + localServerOptions: { + gameMode: 1 + } as any, + preferLoadReadonly: false, + disableLoadPrompts: false, + guestUsername: 'guest', + askGuestName: true, + errorReporting: true, + /** Actually might be useful */ + showCursorBlockInSpectator: false, + renderEntities: true, + smoothLighting: true, + newVersionsLighting: false, + chatSelect: true, + autoJump: 'auto' as 'auto' | 'always' | 'never', + autoParkour: false, + vrSupport: true, // doesn't directly affect the VR mode, should only disable the button which is annoying to android users + vrPageGameRendering: false, + renderDebug: 'basic' as 'none' | 'advanced' | 'basic', + rendererPerfDebugOverlay: false, + + // advanced bot options + autoRespawn: false, + mutedSounds: [] as string[], + plugins: [] as Array<{ enabled: boolean, name: string, description: string, script: string }>, + /** Wether to popup sign editor on server action */ + autoSignEditor: true, + wysiwygSignEditor: 'auto' as 'auto' | 'always' | 'never', + showMinimap: 'never' as 'always' | 'singleplayer' | 'never', + minimapOptimizations: true, + displayBossBars: true, + disabledUiParts: [] as string[], + neighborChunkUpdates: true, + highlightBlockColor: 'auto' as 'auto' | 'blue' | 'classic', + activeRenderer: 'threejs', + rendererSharedOptions: { + _experimentalSmoothChunkLoading: true, + _renderByChunks: false + } +} + +function getDefaultTouchControlsPositions () { + return { + action: [ + 70, + 76 + ], + sneak: [ + 84, + 76 + ], + break: [ + 70, + 57 + ], + jump: [ + 84, + 57 + ], + } as Record<string, [number, number]> +} + +function getTouchControlsSize () { + return { + joystick: 55, + action: 36, + break: 36, + jump: 36, + sneak: 36, + } +} diff --git a/src/globalState.ts b/src/globalState.ts index 671d7907..b8982de7 100644 --- a/src/globalState.ts +++ b/src/globalState.ts @@ -46,6 +46,8 @@ export const showModal = (elem: /* (HTMLElement & Record<string, any>) | */{ re activeModalStack.push(resolved) } +window.showModal = showModal + /** * * @returns true if previous modal was restored diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx index 3409dc76..ba52e333 100644 --- a/src/optionsGuiScheme.tsx +++ b/src/optionsGuiScheme.tsx @@ -20,6 +20,7 @@ import { getVersionAutoSelect } from './connect' import { createNotificationProgressReporter } from './core/progressReporter' import { customKeymaps } from './controls' import { appStorage } from './react/appStorageProvider' +import { exportData, importData } from './core/importExport' export const guiOptionsScheme: { [t in OptionsGroupType]: Array<{ [K in keyof AppOptions]?: Partial<OptionMeta<AppOptions[K]>> } & { custom? }> @@ -532,6 +533,30 @@ export const guiOptionsScheme: { return <Button label='Export/Import...' onClick={() => openOptionsMenu('export-import')} inScreen /> } }, + { + custom () { + const { cookieStorage } = useSnapshot(appStorage) + return <Button + label={`Storage: ${cookieStorage ? 'Synced Cookies' : 'Local Storage'}`} onClick={() => { + appStorage.cookieStorage = !cookieStorage + alert('Reload the page to apply this change') + }} + inScreen + /> + } + }, + { + custom () { + const { cookieStorage } = useSnapshot(appStorage) + return <Button + label={`Storage: ${cookieStorage ? 'Synced Cookies' : 'Local Storage'}`} onClick={() => { + appStorage.cookieStorage = !cookieStorage + alert('Reload the page to apply this change') + }} + inScreen + /> + } + }, { custom () { return <Category>Server Connection</Category> @@ -637,8 +662,7 @@ export const guiOptionsScheme: { custom () { return <Button inScreen - disabled={true} - onClick={() => {}} + onClick={importData} >Import Data</Button> } }, @@ -646,53 +670,7 @@ export const guiOptionsScheme: { custom () { return <Button inScreen - onClick={async () => { - const data = await showInputsModal('Export Profile', { - profileName: { - type: 'text', - }, - exportSettings: { - type: 'checkbox', - defaultValue: true, - }, - exportKeybindings: { - type: 'checkbox', - defaultValue: true, - }, - exportServers: { - type: 'checkbox', - defaultValue: true, - }, - saveUsernameAndProxy: { - type: 'checkbox', - defaultValue: true, - }, - }) - const fileName = `${data.profileName ? `${data.profileName}-` : ''}web-client-profile.json` - const json = { - _about: 'Minecraft Web Client (mcraft.fun) Profile', - ...data.exportSettings ? { - options: getChangedSettings(), - } : {}, - ...data.exportKeybindings ? { - keybindings: customKeymaps, - } : {}, - ...data.exportServers ? { - servers: appStorage.serversList, - } : {}, - ...data.saveUsernameAndProxy ? { - username: appStorage.username, - proxy: appStorage.proxiesData?.selected, - } : {}, - } - const blob = new Blob([JSON.stringify(json, null, 2)], { type: 'application/json' }) - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url - a.download = fileName - a.click() - URL.revokeObjectURL(url) - }} + onClick={exportData} >Export Data</Button> } }, diff --git a/src/optionsStorage.ts b/src/optionsStorage.ts index 882610f8..22d5ef26 100644 --- a/src/optionsStorage.ts +++ b/src/optionsStorage.ts @@ -5,161 +5,10 @@ import { appQueryParams, appQueryParamsArray } from './appParams' import type { AppConfig } from './appConfig' import { appStorage } from './react/appStorageProvider' import { miscUiState } from './globalState' +import { defaultOptions } from './defaultOptions' const isDev = process.env.NODE_ENV === 'development' const initialAppConfig = process.env?.INLINED_APP_CONFIG as AppConfig ?? {} -const defaultOptions = { - renderDistance: 3, - keepChunksDistance: 1, - multiplayerRenderDistance: 3, - closeConfirmation: true, - autoFullScreen: false, - mouseRawInput: true, - autoExitFullscreen: false, - localUsername: 'wanderer', - mouseSensX: 50, - mouseSensY: -1, - chatWidth: 320, - chatHeight: 180, - chatScale: 100, - chatOpacity: 100, - chatOpacityOpened: 100, - messagesLimit: 200, - volume: 50, - enableMusic: false, - // fov: 70, - fov: 75, - guiScale: 3, - autoRequestCompletions: true, - touchButtonsSize: 40, - touchButtonsOpacity: 80, - touchButtonsPosition: 12, - touchControlsPositions: getDefaultTouchControlsPositions(), - touchControlsSize: getTouchControlsSize(), - touchMovementType: 'modern' as 'modern' | 'classic', - touchInteractionType: 'classic' as 'classic' | 'buttons', - gpuPreference: 'default' as 'default' | 'high-performance' | 'low-power', - backgroundRendering: '20fps' as 'full' | '20fps' | '5fps', - /** @unstable */ - disableAssets: false, - /** @unstable */ - debugLogNotFrequentPackets: false, - unimplementedContainers: false, - dayCycleAndLighting: true, - loadPlayerSkins: true, - renderEars: true, - lowMemoryMode: false, - starfieldRendering: true, - enabledResourcepack: null as string | null, - useVersionsTextures: 'latest', - serverResourcePacks: 'prompt' as 'prompt' | 'always' | 'never', - showHand: true, - viewBobbing: true, - displayRecordButton: true, - packetsLoggerPreset: 'all' as 'all' | 'no-buffers', - serversAutoVersionSelect: 'auto' as 'auto' | 'latest' | '1.20.4' | string, - customChannels: false, - remoteContentNotSameOrigin: false as boolean | string[], - packetsRecordingAutoStart: false, - language: 'auto', - preciseMouseInput: false, - // todo ui setting, maybe enable by default? - waitForChunksRender: false as 'sp-only' | boolean, - jeiEnabled: true as boolean | Array<'creative' | 'survival' | 'adventure' | 'spectator'>, - modsSupport: false, - modsAutoUpdate: 'check' as 'check' | 'never' | 'always', - modsUpdatePeriodCheck: 24, // hours - preventBackgroundTimeoutKick: false, - preventSleep: false, - debugContro: false, - debugChatScroll: false, - chatVanillaRestrictions: true, - debugResponseTimeIndicator: false, - chatPingExtension: true, - // antiAliasing: false, - topRightTimeDisplay: 'only-fullscreen' as 'only-fullscreen' | 'always' | 'never', - - clipWorldBelowY: undefined as undefined | number, // will be removed - disableSignsMapsSupport: false, - singleplayerAutoSave: false, - showChunkBorders: false, // todo rename option - frameLimit: false as number | false, - alwaysBackupWorldBeforeLoading: undefined as boolean | undefined | null, - alwaysShowMobileControls: false, - excludeCommunicationDebugEvents: [], - preventDevReloadWhilePlaying: false, - numWorkers: 4, - localServerOptions: { - gameMode: 1 - } as any, - preferLoadReadonly: false, - disableLoadPrompts: false, - guestUsername: 'guest', - askGuestName: true, - errorReporting: true, - /** Actually might be useful */ - showCursorBlockInSpectator: false, - renderEntities: true, - smoothLighting: true, - newVersionsLighting: false, - chatSelect: true, - autoJump: 'auto' as 'auto' | 'always' | 'never', - autoParkour: false, - vrSupport: true, // doesn't directly affect the VR mode, should only disable the button which is annoying to android users - vrPageGameRendering: false, - renderDebug: 'basic' as 'none' | 'advanced' | 'basic', - rendererPerfDebugOverlay: false, - - // advanced bot options - autoRespawn: false, - mutedSounds: [] as string[], - plugins: [] as Array<{ enabled: boolean, name: string, description: string, script: string }>, - /** Wether to popup sign editor on server action */ - autoSignEditor: true, - wysiwygSignEditor: 'auto' as 'auto' | 'always' | 'never', - showMinimap: 'never' as 'always' | 'singleplayer' | 'never', - minimapOptimizations: true, - displayBossBars: true, - disabledUiParts: [] as string[], - neighborChunkUpdates: true, - highlightBlockColor: 'auto' as 'auto' | 'blue' | 'classic', - activeRenderer: 'threejs', - rendererSharedOptions: { - _experimentalSmoothChunkLoading: true, - _renderByChunks: false - } -} - -function getDefaultTouchControlsPositions () { - return { - action: [ - 70, - 76 - ], - sneak: [ - 84, - 76 - ], - break: [ - 70, - 57 - ], - jump: [ - 84, - 57 - ], - } as Record<string, [number, number]> -} - -function getTouchControlsSize () { - return { - joystick: 55, - action: 36, - break: 36, - jump: 36, - sneak: 36, - } -} // const qsOptionsRaw = new URLSearchParams(location.search).getAll('setting') const qsOptionsRaw = appQueryParamsArray.setting ?? [] @@ -191,15 +40,15 @@ const migrateOptions = (options: Partial<AppOptions & Record<string, any>>) => { return options } const migrateOptionsLocalStorage = () => { - if (Object.keys(appStorage.options).length) { - for (const key of Object.keys(appStorage.options)) { + if (Object.keys(appStorage['options'] ?? {}).length) { + for (const key of Object.keys(appStorage['options'])) { if (!(key in defaultOptions)) continue // drop unknown options const defaultValue = defaultOptions[key] - if (JSON.stringify(defaultValue) !== JSON.stringify(appStorage.options[key])) { - appStorage.changedSettings[key] = appStorage.options[key] + if (JSON.stringify(defaultValue) !== JSON.stringify(appStorage['options'][key])) { + appStorage.changedSettings[key] = appStorage['options'][key] } } - appStorage.options = {} + delete appStorage['options'] } } @@ -311,3 +160,5 @@ export const getAppLanguage = () => { } return options.language } + +export { defaultOptions } from './defaultOptions' diff --git a/src/react/StorageConflictModal.tsx b/src/react/StorageConflictModal.tsx new file mode 100644 index 00000000..9e20ca2d --- /dev/null +++ b/src/react/StorageConflictModal.tsx @@ -0,0 +1,94 @@ +import { useSnapshot } from 'valtio' +import { activeModalStack, hideCurrentModal } from '../globalState' +import { resolveStorageConflicts, getStorageConflicts } from './appStorageProvider' +import { useIsModalActive } from './utilsApp' +import Screen from './Screen' +import Button from './Button' + +const formatTimestamp = (timestamp?: number) => { + if (!timestamp) return 'Unknown time' + return new Date(timestamp).toLocaleString() +} + +export default () => { + const isModalActive = useIsModalActive('storage-conflict') + const conflicts = getStorageConflicts() + + if (!isModalActive/* || conflicts.length === 0 */) return null + + const conflictText = conflicts.map(conflict => { + const localTime = formatTimestamp(conflict.localStorageTimestamp) + const cookieTime = formatTimestamp(conflict.cookieTimestamp) + return `${conflict.key}: LocalStorage (${localTime}) vs Cookie (${cookieTime})` + }).join('\n') + + return ( + <div + > + <div style={{ + background: '#dcb58f', + border: '2px solid #654321', + padding: '20px', + margin: '10px', + color: '#FFFFFF', + fontFamily: 'minecraft, monospace', + textAlign: 'center' + }}> + <div style={{ + fontSize: '16px', + fontWeight: 'bold', + color: '#000000', + marginBottom: '15px' + }}> + Data Conflict Found + </div> + + <div style={{ + fontSize: '12px', + marginBottom: '20px', + whiteSpace: 'pre-line', + // backgroundColor: 'rgba(0, 0, 0, 0.5)', + color: '#642323', + padding: '10px', + // border: '1px solid #654321' + }}> + You have conflicting data between localStorage (old) and cookies (new, domain-synced) for the following settings: + {'\n\n'} + {conflictText} + {'\n\n'} + Please choose which version to keep: + </div> + + <div style={{ display: 'flex', gap: '10px', justifyContent: 'center', fontSize: '8px', color: 'black' }}> + <div + onClick={() => { + resolveStorageConflicts(true) // Use localStorage + hideCurrentModal() + }} + style={{ + border: '1px solid #654321', + padding: '8px 16px', + cursor: 'pointer' + }} + > + Use Local Storage & Disable Cookie Sync + </div> + + <div + onClick={() => { + resolveStorageConflicts(false) // Use cookies + hideCurrentModal() + }} + style={{ + border: '1px solid #654321', + padding: '8px 16px', + cursor: 'pointer' + }} + > + Use Cookie Data & Remove Local Data + </div> + </div> + </div> + </div> + ) +} diff --git a/src/react/appStorageProvider.ts b/src/react/appStorageProvider.ts index 499ec71c..bee8e408 100644 --- a/src/react/appStorageProvider.ts +++ b/src/react/appStorageProvider.ts @@ -8,7 +8,9 @@ import type { BaseServerInfo } from './AddServerOrConnect' // when opening html file locally in browser, localStorage is shared between all ever opened html files, so we try to avoid conflicts const localStoragePrefix = process.env?.SINGLE_FILE_BUILD ? 'minecraft-web-client:' : '' +const cookiePrefix = '' const { localStorage } = window +const migrateRemoveLocalStorage = false export interface SavedProxiesData { proxies: string[] @@ -31,12 +33,19 @@ export interface StoreServerItem extends BaseServerInfo { isRecommended?: boolean } +interface StorageConflict { + key: string + localStorageValue: any + localStorageTimestamp?: number + cookieValue: any + cookieTimestamp?: number +} + type StorageData = { + cookieStorage: boolean | { ignoreKeys: Array<keyof StorageData> } customCommands: Record<string, CustomCommand> | undefined username: string | undefined keybindings: UserOverridesConfig | undefined - /** @deprecated */ - options: any changedSettings: any proxiesData: SavedProxiesData | undefined serversHistory: ServerHistoryEntry[] @@ -46,10 +55,115 @@ type StorageData = { firstModsPageVisit: boolean } +const cookieStoreKeys: Array<keyof StorageData> = [ + 'customCommands', + 'username', + 'keybindings', + 'changedSettings', + 'serversList', +] + const oldKeysAliases: Partial<Record<keyof StorageData, string>> = { serversHistory: 'serverConnectionHistory', } +// Cookie storage functions +const getCookieValue = (key: string): string | null => { + const cookie = document.cookie.split(';').find(c => c.trimStart().startsWith(`${cookiePrefix}${key}=`)) + if (cookie) { + return decodeURIComponent(cookie.split('=')[1]) + } + return null +} + +const topLevelDomain = window.location.hostname.split('.').slice(-2).join('.') +const cookieBase = `; Domain=.${topLevelDomain}; Path=/; SameSite=Strict; Secure` + +const setCookieValue = (key: string, value: string): boolean => { + try { + const cookieKey = `${cookiePrefix}${key}` + let cookie = `${cookieKey}=${encodeURIComponent(value)}` + cookie += `${cookieBase}; Max-Age=2147483647` + + // Test if cookie exceeds size limit + if (cookie.length > 4096) { + throw new Error(`Cookie size limit exceeded for key '${key}'. Cookie size: ${cookie.length} bytes, limit: 4096 bytes.`) + } + + document.cookie = cookie + return true + } catch (error) { + console.error(`Failed to set cookie for key '${key}':`, error) + window.showNotification(`Failed to save data to cookies: ${error.message}`, 'Consider switching to localStorage in advanced settings.', true) + return false + } +} + +const deleteCookie = (key: string) => { + const cookieKey = `${cookiePrefix}${key}` + document.cookie = `${cookieKey}=; ${cookieBase}; expires=Thu, 01 Jan 1970 00:00:00 UTC;` +} + +// Storage conflict detection and resolution +let storageConflicts: StorageConflict[] = [] + +const detectStorageConflicts = (): StorageConflict[] => { + const conflicts: StorageConflict[] = [] + + for (const key of cookieStoreKeys) { + const localStorageKey = `${localStoragePrefix}${key}` + const localStorageValue = localStorage.getItem(localStorageKey) + const cookieValue = getCookieValue(key) + + if (localStorageValue && cookieValue) { + try { + const localParsed = JSON.parse(localStorageValue) + const cookieParsed = JSON.parse(cookieValue) + + if (localParsed?.migrated) { + continue + } + + // Extract timestamps if they exist + const localTimestamp = localParsed?.timestamp + const cookieTimestamp = cookieParsed?.timestamp + + // Compare the actual data (excluding timestamp) + const localData = localTimestamp ? { ...localParsed } : localParsed + const cookieData = cookieTimestamp ? { ...cookieParsed } : cookieParsed + delete localData.timestamp + delete cookieData.timestamp + + if (JSON.stringify(localData) !== JSON.stringify(cookieData)) { + conflicts.push({ + key, + localStorageValue: localData, + localStorageTimestamp: localTimestamp, + cookieValue: cookieData, + cookieTimestamp + }) + } + } catch (e) { + console.error(`Failed to parse storage values for conflict detection on key '${key}':`, e, localStorageValue, cookieValue) + } + } + } + + return conflicts +} + +const showStorageConflictModal = () => { + // Import showModal dynamically to avoid circular dependency + const showModal = (window as any).showModal || ((modal: any) => { + console.error('Modal system not available:', modal) + console.warn('Storage conflicts detected but modal system not available:', storageConflicts) + }) + + setTimeout(() => { + showModal({ reactType: 'storage-conflict', conflicts: storageConflicts }) + }, 100) +} + const migrateLegacyData = () => { const proxies = localStorage.getItem('proxies') const selectedProxy = localStorage.getItem('selectedProxy') @@ -75,10 +189,10 @@ const migrateLegacyData = () => { } const defaultStorageData: StorageData = { + cookieStorage: !!process.env.ENABLE_COOKIE_STORAGE && !process.env?.SINGLE_FILE_BUILD, customCommands: undefined, username: undefined, keybindings: undefined, - options: {}, changedSettings: {}, proxiesData: undefined, serversHistory: [], @@ -108,29 +222,140 @@ export const getRandomUsername = (appConfig: AppConfig) => { export const appStorage = proxy({ ...defaultStorageData }) -// Restore data from localStorage -for (const key of Object.keys(defaultStorageData)) { - const prefixedKey = `${localStoragePrefix}${key}` - const aliasedKey = oldKeysAliases[key] - const storedValue = localStorage.getItem(prefixedKey) ?? (aliasedKey ? localStorage.getItem(aliasedKey) : undefined) - if (storedValue) { +// Check if cookie storage should be used (will be set by options) +const shouldUseCookieStorage = () => { + const isSecureCookiesAvailable = () => { + // either https or localhost + return window.location.protocol === 'https:' || window.location.hostname === 'localhost' + } + if (!isSecureCookiesAvailable()) { + return false + } + + const localStorageValue = localStorage.getItem(`${localStoragePrefix}cookieStorage`) + if (localStorageValue === null) { + return appStorage.cookieStorage === true + } + return localStorageValue === 'true' +} + +// Restore data from storage with conflict detection +const restoreStorageData = () => { + const useCookieStorage = shouldUseCookieStorage() + + if (useCookieStorage) { + // Detect conflicts first + storageConflicts = detectStorageConflicts() + + if (storageConflicts.length > 0) { + // Show conflict resolution modal + showStorageConflictModal() + return // Don't restore data until conflict is resolved + } + } + + for (const key of Object.keys(defaultStorageData)) { + const typedKey = key + const prefixedKey = `${localStoragePrefix}${key}` + const aliasedKey = oldKeysAliases[typedKey] + + let storedValue: string | null = null + let cookieValueCanBeUsed = false + let usingLocalStorageValue = false + + // Try cookie storage first if enabled and key is in cookieStoreKeys + if (useCookieStorage && cookieStoreKeys.includes(typedKey)) { + storedValue = getCookieValue(key) + cookieValueCanBeUsed = true + } + + // Fallback to localStorage if no cookie value found + if (storedValue === null) { + storedValue = localStorage.getItem(prefixedKey) ?? (aliasedKey ? localStorage.getItem(aliasedKey) : null) + usingLocalStorageValue = true + } + + if (storedValue) { + try { + let parsed = JSON.parse(storedValue) + + // Handle timestamped data + if (parsed && typeof parsed === 'object' && parsed.timestamp) { + delete parsed.timestamp + // If it was a wrapped primitive, unwrap it + if ('data' in parsed && Object.keys(parsed).length === 1) { + parsed = parsed.data + } + } + + appStorage[typedKey] = parsed + + if (usingLocalStorageValue && cookieValueCanBeUsed) { + // migrate localStorage to cookie + saveKey(key) + markLocalStorageAsMigrated(key) + } + } catch (e) { + console.error(`Failed to parse stored value for ${key}:`, e) + } + } + } +} + +const markLocalStorageAsMigrated = (key: keyof StorageData) => { + const localStorageKey = `${localStoragePrefix}${key}` + if (migrateRemoveLocalStorage) { + localStorage.removeItem(localStorageKey) + return + } + + const data = localStorage.getItem(localStorageKey) + if (data) { try { - const parsed = JSON.parse(storedValue) - // appStorage[key] = parsed && typeof parsed === 'object' ? ref(parsed) : parsed - appStorage[key] = parsed - } catch (e) { - console.error(`Failed to parse stored value for ${key}:`, e) + localStorage.setItem(localStorageKey, JSON.stringify({ ...JSON.parse(data), migrated: Date.now() })) + } catch (err) { } } } const saveKey = (key: keyof StorageData) => { + const useCookieStorage = shouldUseCookieStorage() const prefixedKey = `${localStoragePrefix}${key}` const value = appStorage[key] - if (value === undefined) { - localStorage.removeItem(prefixedKey) - } else { - localStorage.setItem(prefixedKey, JSON.stringify(value)) + + const dataToSave = value === undefined ? undefined : ( + value && typeof value === 'object' && !Array.isArray(value) + ? { ...value, timestamp: Date.now() } + : { data: value, timestamp: Date.now() } + ) + + const serialized = dataToSave === undefined ? undefined : JSON.stringify(dataToSave) + + let useLocalStorage = true + // Save to cookie if enabled and key is in cookieStoreKeys + if (useCookieStorage && cookieStoreKeys.includes(key)) { + useLocalStorage = false + if (serialized === undefined) { + deleteCookie(key) + } else { + const success = setCookieValue(key, serialized) + if (success) { + // Remove from localStorage if cookie save was successful + markLocalStorageAsMigrated(key) + } else { + // Disabling for now so no confusing conflicts modal after page reload + // useLocalStorage = true + } + } + } + + if (useLocalStorage) { + // Save to localStorage + if (value === undefined) { + localStorage.removeItem(prefixedKey) + } else { + localStorage.setItem(prefixedKey, JSON.stringify(value)) + } } } @@ -141,7 +366,6 @@ subscribe(appStorage, (ops) => { saveKey(key as keyof StorageData) } }) -// Subscribe to changes and save to localStorage export const resetAppStorage = () => { for (const key of Object.keys(appStorage)) { @@ -153,6 +377,38 @@ export const resetAppStorage = () => { localStorage.removeItem(key) } } + + if (!shouldUseCookieStorage()) return + const shouldContinue = window.confirm(`Removing all synced cookies will remove all data from all ${topLevelDomain} subdomains websites. Continue?`) + if (!shouldContinue) return + + // Clear cookies + for (const key of cookieStoreKeys) { + deleteCookie(key) + } } +// Export functions for conflict resolution +export const resolveStorageConflicts = (useLocalStorage: boolean) => { + if (useLocalStorage) { + // Disable cookie storage and use localStorage data + appStorage.cookieStorage = false + } else { + // Remove localStorage data and continue using cookie storage + for (const conflict of storageConflicts) { + const prefixedKey = `${localStoragePrefix}${conflict.key}` + localStorage.removeItem(prefixedKey) + } + } + + // Clear conflicts and restore data + storageConflicts = [] + restoreStorageData() +} + +export const getStorageConflicts = () => storageConflicts + migrateLegacyData() + +// Restore data after checking for conflicts +restoreStorageData() diff --git a/src/reactUi.tsx b/src/reactUi.tsx index 8e94b35d..59ed9124 100644 --- a/src/reactUi.tsx +++ b/src/reactUi.tsx @@ -65,6 +65,7 @@ import RendererDebugMenu from './react/RendererDebugMenu' import CreditsAboutModal from './react/CreditsAboutModal' import GlobalOverlayHints from './react/GlobalOverlayHints' import FullscreenTime from './react/FullscreenTime' +import StorageConflictModal from './react/StorageConflictModal' const isFirefox = ua.getBrowser().name === 'Firefox' if (isFirefox) { @@ -247,6 +248,7 @@ const App = () => { <SelectOption /> <CreditsAboutModal /> + <StorageConflictModal /> <NoModalFoundProvider /> </RobustPortal> <RobustPortal to={document.body}> From af5a0b2835312945ea251014ba231977070bb70a Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Fri, 27 Jun 2025 16:50:44 +0300 Subject: [PATCH 048/181] fix: fix camera desync updates in 3rd view and starfield --- renderer/viewer/lib/worldrendererCommon.ts | 6 +-- renderer/viewer/three/entities.ts | 2 +- renderer/viewer/three/worldrendererThree.ts | 47 +++++++++++++++++---- 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/renderer/viewer/lib/worldrendererCommon.ts b/renderer/viewer/lib/worldrendererCommon.ts index dfa4f43b..4441201c 100644 --- a/renderer/viewer/lib/worldrendererCommon.ts +++ b/renderer/viewer/lib/worldrendererCommon.ts @@ -106,7 +106,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> }> customTexturesDataUrl = undefined as string | undefined workers: any[] = [] - viewerPosition?: Vec3 + viewerChunkPosition?: Vec3 lastCamUpdate = 0 droppedFpsPercentage = 0 initialChunkLoadWasStartedIn: number | undefined @@ -499,7 +499,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> timeUpdated? (newTime: number): void updateViewerPosition (pos: Vec3) { - this.viewerPosition = pos + this.viewerChunkPosition = pos for (const [key, value] of Object.entries(this.loadedChunks)) { if (!value) continue this.updatePosDataChunk?.(key) @@ -513,7 +513,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> } getDistance (posAbsolute: Vec3) { - const [botX, botZ] = chunkPos(this.viewerPosition!) + const [botX, botZ] = chunkPos(this.viewerChunkPosition!) const dx = Math.abs(botX - Math.floor(posAbsolute.x / 16)) const dz = Math.abs(botZ - Math.floor(posAbsolute.z / 16)) return [dx, dz] as [number, number] diff --git a/renderer/viewer/three/entities.ts b/renderer/viewer/three/entities.ts index fea7f710..b4de4f1b 100644 --- a/renderer/viewer/three/entities.ts +++ b/renderer/viewer/three/entities.ts @@ -344,7 +344,7 @@ export class Entities { } const dt = this.clock.getDelta() - const botPos = this.worldRenderer.viewerPosition + const botPos = this.worldRenderer.viewerChunkPosition const VISIBLE_DISTANCE = 10 * 10 // Update regular entities diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index b4ae4961..c4482052 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -88,7 +88,7 @@ export class WorldRendererThree extends WorldRendererCommon { this.renderer = renderer displayOptions.rendererState.renderer = WorldRendererThree.getRendererInfo(renderer) ?? '...' - this.starField = new StarField(this.scene) + this.starField = new StarField(this) this.cursorBlock = new CursorBlock(this) this.holdingBlock = new HoldingBlock(this) this.holdingBlockLeft = new HoldingBlock(this, true) @@ -318,10 +318,11 @@ export class WorldRendererThree extends WorldRendererCommon { section.renderOrder = 500 - chunkDistance } - updateViewerPosition (pos: Vec3): void { - this.viewerPosition = pos - const cameraPos = this.cameraObject.position.toArray().map(x => Math.floor(x / 16)) as [number, number, number] - this.cameraSectionPos = new Vec3(...cameraPos) + override updateViewerPosition (pos: Vec3): void { + this.viewerChunkPosition = pos + } + + cameraSectionPositionUpdate () { // eslint-disable-next-line guard-for-in for (const key in this.sectionObjects) { const value = this.sectionObjects[key] @@ -447,11 +448,35 @@ export class WorldRendererThree extends WorldRendererCommon { return tex } + getCameraPosition () { + const worldPos = new THREE.Vector3() + this.camera.getWorldPosition(worldPos) + return worldPos + } + + getWorldCameraPosition () { + const pos = this.getCameraPosition() + return new Vec3( + Math.floor(pos.x / 16), + Math.floor(pos.y / 16), + Math.floor(pos.z / 16) + ) + } + + updateCameraSectionPos () { + const newSectionPos = this.getWorldCameraPosition() + if (!this.cameraSectionPos.equals(newSectionPos)) { + this.cameraSectionPos = newSectionPos + this.cameraSectionPositionUpdate() + } + } + setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number) { const yOffset = this.playerStateReactive.eyeHeight this.updateCamera(pos?.offset(0, yOffset, 0) ?? null, yaw, pitch) this.media.tryIntersectMedia() + this.updateCameraSectionPos() } getThirdPersonCamera (pos: THREE.Vector3 | null, yaw: number, pitch: number) { @@ -636,6 +661,8 @@ export class WorldRendererThree extends WorldRendererCommon { } } } + + this.updateCameraSectionPos() } debugChunksVisibilityOverride () { @@ -988,7 +1015,9 @@ class StarField { } } - constructor (private readonly scene: THREE.Scene) { + constructor ( + private readonly worldRenderer: WorldRendererThree + ) { } addToScene () { @@ -1030,11 +1059,11 @@ class StarField { // Create points and add them to the scene this.points = new THREE.Points(geometry, material) - this.scene.add(this.points) + this.worldRenderer.scene.add(this.points) const clock = new THREE.Clock() this.points.onBeforeRender = (renderer, scene, camera) => { - this.points?.position.copy?.(camera.position) + this.points?.position.copy?.(this.worldRenderer.getCameraPosition()) material.uniforms.time.value = clock.getElapsedTime() * speed } this.points.renderOrder = -1 @@ -1044,7 +1073,7 @@ class StarField { if (this.points) { this.points.geometry.dispose(); (this.points.material as THREE.Material).dispose() - this.scene.remove(this.points) + this.worldRenderer.scene.remove(this.points) this.points = undefined } From 83d783226fb2c726aac107b0c3c6b0b59727bdfc Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Fri, 27 Jun 2025 18:08:03 +0300 Subject: [PATCH 049/181] fix migration marking --- src/react/appStorageProvider.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/react/appStorageProvider.ts b/src/react/appStorageProvider.ts index bee8e408..a9bac660 100644 --- a/src/react/appStorageProvider.ts +++ b/src/react/appStorageProvider.ts @@ -312,7 +312,12 @@ const markLocalStorageAsMigrated = (key: keyof StorageData) => { const data = localStorage.getItem(localStorageKey) if (data) { try { - localStorage.setItem(localStorageKey, JSON.stringify({ ...JSON.parse(data), migrated: Date.now() })) + const parsed = JSON.parse(data) + localStorage.setItem( + localStorageKey, JSON.stringify(typeof parsed === 'object' ? { + ...parsed, migrated: Date.now() + } : { data: parsed, migrated: Date.now() }) + ) } catch (err) { } } From 3336680a0ee5d8b3f5bc030388de19f57c17373c Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Fri, 27 Jun 2025 18:08:33 +0300 Subject: [PATCH 050/181] fix z index of modal --- src/react/StorageConflictModal.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/react/StorageConflictModal.tsx b/src/react/StorageConflictModal.tsx index 9e20ca2d..0b4224f3 100644 --- a/src/react/StorageConflictModal.tsx +++ b/src/react/StorageConflictModal.tsx @@ -32,7 +32,8 @@ export default () => { margin: '10px', color: '#FFFFFF', fontFamily: 'minecraft, monospace', - textAlign: 'center' + textAlign: 'center', + zIndex: 1000, }}> <div style={{ fontSize: '16px', From 0e4435ef911a6160037dcdb970bcd3ee48a3fccb Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Fri, 27 Jun 2025 22:06:10 +0300 Subject: [PATCH 051/181] feat: add support for /ping command, fix chat fading! --- src/react/Chat.css | 11 ++++--- src/react/Chat.tsx | 47 +++++++++++++++++------------- src/react/ChatProvider.tsx | 23 +++++++++++---- src/react/StorageConflictModal.tsx | 3 ++ src/react/appStorageProvider.ts | 15 ++-------- src/reactUi.tsx | 2 +- 6 files changed, 55 insertions(+), 46 deletions(-) diff --git a/src/react/Chat.css b/src/react/Chat.css index f1e92338..47394948 100644 --- a/src/react/Chat.css +++ b/src/react/Chat.css @@ -189,23 +189,22 @@ input[type=text], background-color: rgba(0, 0, 0, 0.5); list-style: none; overflow-wrap: break-word; -} - -.chat-message-fadeout { opacity: 1; - transition: all 3s; } -.chat-message-fade { +.chat-message-fading { opacity: 0; + transition: opacity 3s ease-in-out; } .chat-message-faded { - transition: none !important; + display: none; } +/* Ensure messages are always visible when chat is open */ .chat.opened .chat-message { opacity: 1 !important; + display: block !important; transition: none !important; } diff --git a/src/react/Chat.tsx b/src/react/Chat.tsx index 41a6cb6f..7c1c8633 100644 --- a/src/react/Chat.tsx +++ b/src/react/Chat.tsx @@ -11,19 +11,37 @@ import { useScrollBehavior } from './hooks/useScrollBehavior' export type Message = { parts: MessageFormatPart[], id: number - fading?: boolean - faded?: boolean + timestamp?: number } -const MessageLine = ({ message, currentPlayerName }: { message: Message, currentPlayerName?: string }) => { +const MessageLine = ({ message, currentPlayerName, chatOpened }: { message: Message, currentPlayerName?: string, chatOpened?: boolean }) => { + const [fadeState, setFadeState] = useState<'visible' | 'fading' | 'faded'>('visible') + + useEffect(() => { + // Start fading after 5 seconds + const fadeTimeout = setTimeout(() => { + setFadeState('fading') + }, 5000) + + // Remove after fade animation (3s) completes + const removeTimeout = setTimeout(() => { + setFadeState('faded') + }, 8000) + + // Cleanup timeouts if component unmounts + return () => { + clearTimeout(fadeTimeout) + clearTimeout(removeTimeout) + } + }, []) // Empty deps array since we only want this to run once when message is added + const classes = { - 'chat-message-fadeout': message.fading, - 'chat-message-fade': message.fading, - 'chat-message-faded': message.faded, - 'chat-message': true + 'chat-message': true, + 'chat-message-fading': !chatOpened && fadeState === 'fading', + 'chat-message-faded': !chatOpened && fadeState === 'faded' } - return <li className={Object.entries(classes).filter(([, val]) => val).map(([name]) => name).join(' ')}> + return <li className={Object.entries(classes).filter(([, val]) => val).map(([name]) => name).join(' ')} data-time={message.timestamp ? new Date(message.timestamp).toLocaleString('en-US', { hour12: false }) : undefined}> {message.parts.map((msg, i) => { // Check if this is a text part that might contain a mention if (msg.text && currentPlayerName) { @@ -70,17 +88,6 @@ export const chatInputValueGlobal = proxy({ value: '' }) -export const fadeMessage = (message: Message, initialTimeout: boolean, requestUpdate: () => void) => { - setTimeout(() => { - message.fading = true - requestUpdate() - setTimeout(() => { - message.faded = true - requestUpdate() - }, 3000) - }, initialTimeout ? 5000 : 0) -} - export default ({ messages, opacity = 1, @@ -372,7 +379,7 @@ export default ({ </div> )} {messages.map((m) => ( - <MessageLine key={reactKeyForMessage(m)} message={m} currentPlayerName={playerNameValidated} /> + <MessageLine key={reactKeyForMessage(m)} message={m} currentPlayerName={playerNameValidated} chatOpened={opened} /> ))} </div> || undefined} </div> diff --git a/src/react/ChatProvider.tsx b/src/react/ChatProvider.tsx index 83691b83..0bb13285 100644 --- a/src/react/ChatProvider.tsx +++ b/src/react/ChatProvider.tsx @@ -5,7 +5,7 @@ import { getBuiltinCommandsList, tryHandleBuiltinCommand } from '../builtinComma import { gameAdditionalState, hideCurrentModal, miscUiState } from '../globalState' import { options } from '../optionsStorage' import { viewerVersionState } from '../viewerConnector' -import Chat, { Message, fadeMessage } from './Chat' +import Chat, { Message } from './Chat' import { useIsModalActive } from './utilsApp' import { hideNotification, notificationProxy, showNotification } from './NotificationProvider' import { getServerIndex, updateLoadedServerData } from './serversStorage' @@ -16,6 +16,7 @@ export default () => { const [messages, setMessages] = useState([] as Message[]) const isChatActive = useIsModalActive('chat') const lastMessageId = useRef(0) + const lastPingTime = useRef(0) const usingTouch = useSnapshot(miscUiState).currentTouch const { chatSelect, messagesLimit, chatOpacity, chatOpacityOpened, chatVanillaRestrictions, debugChatScroll, chatPingExtension } = useSnapshot(options) const isUsingMicrosoftAuth = useMemo(() => !!lastConnectOptions.value?.authenticatedAccount, []) @@ -29,18 +30,23 @@ export default () => { jsonMsg = jsonMsg['unsigned'] } const parts = formatMessage(jsonMsg) + const messageText = parts.map(part => part.text).join('') + + // Handle ping response + if (messageText === 'Pong!' && lastPingTime.current > 0) { + const latency = Date.now() - lastPingTime.current + parts.push({ text: ` Latency: ${latency}ms`, color: '#00ff00' }) + lastPingTime.current = 0 + } setMessages(m => { lastMessageId.current++ const newMessage: Message = { parts, id: lastMessageId.current, - faded: false, + timestamp: Date.now() } - fadeMessage(newMessage, true, () => { - // eslint-disable-next-line max-nested-callbacks - setMessages(m => [...m]) - }) + return [...m, newMessage].slice(-messagesLimit) }) }) @@ -61,6 +67,11 @@ export default () => { return players.filter(name => (!value || name.toLowerCase().includes(value.toLowerCase())) && name !== bot.username).map(name => `@${name}`) }} sendMessage={async (message) => { + // Record ping command time + if (message === '/ping') { + lastPingTime.current = Date.now() + } + const builtinHandled = tryHandleBuiltinCommand(message) if (getServerIndex() !== undefined && (message.startsWith('/login') || message.startsWith('/register'))) { showNotification('Click here to save your password in browser for auto-login', undefined, false, undefined, () => { diff --git a/src/react/StorageConflictModal.tsx b/src/react/StorageConflictModal.tsx index 0b4224f3..ac78d90a 100644 --- a/src/react/StorageConflictModal.tsx +++ b/src/react/StorageConflictModal.tsx @@ -34,6 +34,9 @@ export default () => { fontFamily: 'minecraft, monospace', textAlign: 'center', zIndex: 1000, + position: 'fixed', + left: 0, + right: 0 }}> <div style={{ fontSize: '16px', diff --git a/src/react/appStorageProvider.ts b/src/react/appStorageProvider.ts index a9bac660..c65cc701 100644 --- a/src/react/appStorageProvider.ts +++ b/src/react/appStorageProvider.ts @@ -120,7 +120,7 @@ const detectStorageConflicts = (): StorageConflict[] => { const localParsed = JSON.parse(localStorageValue) const cookieParsed = JSON.parse(cookieValue) - if (localParsed?.migrated) { + if (localStorage.getItem(`${localStorageKey}:migrated`)) { continue } @@ -309,18 +309,7 @@ const markLocalStorageAsMigrated = (key: keyof StorageData) => { return } - const data = localStorage.getItem(localStorageKey) - if (data) { - try { - const parsed = JSON.parse(data) - localStorage.setItem( - localStorageKey, JSON.stringify(typeof parsed === 'object' ? { - ...parsed, migrated: Date.now() - } : { data: parsed, migrated: Date.now() }) - ) - } catch (err) { - } - } + localStorage.setItem(`${localStorageKey}:migrated`, 'true') } const saveKey = (key: keyof StorageData) => { diff --git a/src/reactUi.tsx b/src/reactUi.tsx index 59ed9124..b15cb79d 100644 --- a/src/reactUi.tsx +++ b/src/reactUi.tsx @@ -229,6 +229,7 @@ const App = () => { <div /> </RobustPortal> <EnterFullscreenButton /> + <StorageConflictModal /> <InGameUi /> <RobustPortal to={document.querySelector('#ui-root')}> <AllWidgets /> @@ -248,7 +249,6 @@ const App = () => { <SelectOption /> <CreditsAboutModal /> - <StorageConflictModal /> <NoModalFoundProvider /> </RobustPortal> <RobustPortal to={document.body}> From e161426caf15a29439b4bc77987c3630b1733c6f Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Fri, 27 Jun 2025 22:11:49 +0300 Subject: [PATCH 052/181] always dipslay close buttons from settings --- src/react/OptionsItems.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/react/OptionsItems.tsx b/src/react/OptionsItems.tsx index 4759b3d7..32f99b9e 100644 --- a/src/react/OptionsItems.tsx +++ b/src/react/OptionsItems.tsx @@ -189,18 +189,16 @@ interface Props { } export default ({ items, title, backButtonAction }: Props) => { - const { currentTouch } = useSnapshot(miscUiState) return <Screen title={title} > <div className='screen-items'> - {currentTouch && ( - <div style={{ position: 'fixed', marginLeft: '-30px', display: 'flex', flexDirection: 'column', gap: 1, }}> - <Button icon={pixelartIcons['close']} onClick={hideAllModals} style={{ color: '#ff5d5d', }} /> - <Button icon={pixelartIcons['chevron-left']} onClick={backButtonAction} style={{ color: 'yellow', }} /> - </div> - )} + <div style={{ position: 'fixed', marginLeft: '-30px', display: 'flex', flexDirection: 'column', gap: 1, }}> + <Button icon={pixelartIcons['close']} onClick={hideAllModals} style={{ color: '#ff5d5d', }} /> + <Button icon={pixelartIcons['chevron-left']} onClick={backButtonAction} style={{ color: 'yellow', }} /> + </div> + {items.map((element, i) => { // make sure its unique! return <RenderOption key={element.id ?? `${title}-${i}`} item={element} /> From 369166e0942cf234d34f31b587c4a08424129b09 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Sat, 28 Jun 2025 00:45:54 +0300 Subject: [PATCH 053/181] fix tsc, up readme --- README.MD | 6 ++++-- src/react/Chat.stories.tsx | 11 +---------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/README.MD b/README.MD index 4769192a..5f7a83f6 100644 --- a/README.MD +++ b/README.MD @@ -6,9 +6,11 @@ Minecraft **clone** rewritten in TypeScript using the best modern web technologi You can try this out at [mcraft.fun](https://mcraft.fun/), [pcm.gg](https://pcm.gg) (short link), [mcon.vercel.app](https://mcon.vercel.app/) or the GitHub pages deploy. Every commit from the default (`develop`) branch is deployed to [s.mcraft.fun](https://s.mcraft.fun/) and [s.pcm.gg](https://s.pcm.gg/) - so it's usually newer, but might be less stable. -Don't confuse with [Eaglercraft](https://git.eaglercraft.rip/eaglercraft/eaglercraft-1.8) which is a REAL vanilla Minecraft Java edition port to the web (but with its own limitations). Eaglercraft is a fully playable solution, but this project is more in position of a "technical demo" to show how it's possible to build games for web at scale entirely with the JS ecosystem. Have fun! +> For Turkey/Russia use [ru.mcraft.fun](https://ru.mcraft.fun/) (since Cloudflare is blocked) -For building the project yourself / contributing, see [Development, Debugging & Contributing](#development-debugging--contributing). For reference at what and how web technologies / frameworks are used, see [TECH.md](./TECH.md). +Don't confuse with [Eaglercraft](https://git.eaglercraft.rip/eaglercraft/eaglercraft-1.8) which is a REAL vanilla Minecraft Java edition port to the web (but with its own limitations). Eaglercraft is a fully playable solution, meanwhile this project is aimed for *device-compatiiblity* and better performance so it feels portable, flexible and lightweight. It's also a very strong example on how to build true HTML games for the web at scale entirely with the JS ecosystem. Have fun! + +For building the project yourself / contributing, see [Development, Debugging & Contributing](#development-debugging--contributing). For reference at what and how web technologies / frameworks are used, see [TECH.md](./TECH.md) (also for comparison with Eaglercraft). ### Big Features diff --git a/src/react/Chat.stories.tsx b/src/react/Chat.stories.tsx index 192d5cb4..e1aaf761 100644 --- a/src/react/Chat.stories.tsx +++ b/src/react/Chat.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react' import { useEffect, useState } from 'react' import { formatMessage } from '../chatUtils' -import Chat, { fadeMessage, chatInputValueGlobal } from './Chat' +import Chat, { chatInputValueGlobal } from './Chat' import Button from './Button' window.spamMessage = window.spamMessage ?? '' @@ -63,14 +63,6 @@ const meta: Meta<typeof Chat> = { return () => clearInterval(interval) }, [autoSpam]) - const fadeMessages = () => { - for (const m of messages) { - fadeMessage(m, false, () => { - setMessages([...messages]) - }) - } - } - return <div style={{ marginTop: args.usingTouch ? 100 : 0 }} @@ -88,7 +80,6 @@ const meta: Meta<typeof Chat> = { }} /> <Button onClick={() => setOpen(s => !s)}>Open: {open ? 'on' : 'off'}</Button> - <Button onClick={() => fadeMessages()}>Fade</Button> <Button onClick={() => setAutoSpam(s => !s)}>Auto Spam: {autoSpam ? 'on' : 'off'}</Button> <Button onClick={() => setMessages(args.messages)}>Reset</Button> </div> From fec887c28da80e2f50e28412ae81b89b2c6a76bf Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Sun, 29 Jun 2025 00:56:37 +0300 Subject: [PATCH 054/181] deeply stringify gui items to avoid futher modifications --- pnpm-lock.yaml | 26 +++++++++++++------------- src/inventoryWindows.ts | 13 +++++++++++-- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fbdf89ac..a034c919 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -136,7 +136,7 @@ importers: version: 4.17.21 mcraft-fun-mineflayer: specifier: ^0.1.23 - version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/45f29f9fd1ad6d43d76bbd0b196f2ed4e675772b(encoding@0.1.13)) + version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/0380bed150fe03db4ac37f0194e0cee98356647d(encoding@0.1.13)) minecraft-data: specifier: 3.89.0 version: 3.89.0 @@ -338,10 +338,10 @@ importers: version: 0.2.59 minecraft-inventory-gui: specifier: github:zardoy/minecraft-inventory-gui#next - version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/f425e6a2db796d42cd2c09cf715dad841dec643d(@types/react@18.3.18)(react@18.3.1) + version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/5494f356b1e59eddc876c3ae05ff395f12a46379(@types/react@18.3.18)(react@18.3.1) mineflayer: specifier: github:zardoy/mineflayer#gen-the-master - version: https://codeload.github.com/zardoy/mineflayer/tar.gz/45f29f9fd1ad6d43d76bbd0b196f2ed4e675772b(encoding@0.1.13) + version: https://codeload.github.com/zardoy/mineflayer/tar.gz/0380bed150fe03db4ac37f0194e0cee98356647d(encoding@0.1.13) mineflayer-mouse: specifier: ^0.1.10 version: 0.1.10(@types/debug@4.1.12)(@types/node@22.13.9)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) @@ -6705,8 +6705,8 @@ packages: minecraft-folder-path@1.2.0: resolution: {integrity: sha512-qaUSbKWoOsH9brn0JQuBhxNAzTDMwrOXorwuRxdJKKKDYvZhtml+6GVCUrY5HRiEsieBEjCUnhVpDuQiKsiFaw==} - minecraft-inventory-gui@https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/f425e6a2db796d42cd2c09cf715dad841dec643d: - resolution: {tarball: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/f425e6a2db796d42cd2c09cf715dad841dec643d} + minecraft-inventory-gui@https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/5494f356b1e59eddc876c3ae05ff395f12a46379: + resolution: {tarball: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/5494f356b1e59eddc876c3ae05ff395f12a46379} version: 1.0.1 minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284: @@ -6736,8 +6736,8 @@ packages: resolution: {integrity: sha512-3bxph4jfbkBh5HpeouorxzrfSLNV+i+1gugNJ2jf52HW+rt+tW7eiiFPxrJEsOVkPT/3O/dEIW7j93LRlojMkQ==} engines: {node: '>=22'} - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/45f29f9fd1ad6d43d76bbd0b196f2ed4e675772b: - resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/45f29f9fd1ad6d43d76bbd0b196f2ed4e675772b} + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/0380bed150fe03db4ac37f0194e0cee98356647d: + resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/0380bed150fe03db4ac37f0194e0cee98356647d} version: 4.27.0 engines: {node: '>=22'} @@ -13346,7 +13346,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 transitivePeerDependencies: - supports-color optional: true @@ -16285,7 +16285,7 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 transitivePeerDependencies: - supports-color optional: true @@ -17127,12 +17127,12 @@ snapshots: maxrects-packer: '@zardoy/maxrects-packer@2.7.4' zod: 3.24.2 - mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/45f29f9fd1ad6d43d76bbd0b196f2ed4e675772b(encoding@0.1.13)): + mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/0380bed150fe03db4ac37f0194e0cee98356647d(encoding@0.1.13)): dependencies: '@zardoy/flying-squid': 0.0.49(encoding@0.1.13) exit-hook: 2.2.1 minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) - mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/45f29f9fd1ad6d43d76bbd0b196f2ed4e675772b(encoding@0.1.13) + mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/0380bed150fe03db4ac37f0194e0cee98356647d(encoding@0.1.13) prismarine-item: 1.16.0 ws: 8.18.1 transitivePeerDependencies: @@ -17443,7 +17443,7 @@ snapshots: minecraft-folder-path@1.2.0: {} - minecraft-inventory-gui@https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/f425e6a2db796d42cd2c09cf715dad841dec643d(@types/react@18.3.18)(react@18.3.1): + minecraft-inventory-gui@https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/5494f356b1e59eddc876c3ae05ff395f12a46379(@types/react@18.3.18)(react@18.3.1): dependencies: valtio: 1.13.2(@types/react@18.3.18)(react@18.3.1) transitivePeerDependencies: @@ -17569,7 +17569,7 @@ snapshots: - encoding - supports-color - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/45f29f9fd1ad6d43d76bbd0b196f2ed4e675772b(encoding@0.1.13): + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/0380bed150fe03db4ac37f0194e0cee98356647d(encoding@0.1.13): dependencies: '@nxg-org/mineflayer-physics-util': 1.8.10 minecraft-data: 3.89.0 diff --git a/src/inventoryWindows.ts b/src/inventoryWindows.ts index 05426d15..01d1691d 100644 --- a/src/inventoryWindows.ts +++ b/src/inventoryWindows.ts @@ -159,7 +159,9 @@ const getImage = ({ path = undefined as string | undefined, texture = undefined if (image) { return image } - if (!path && !texture) throw new Error('Either pass path or texture') + if (!path && !texture) { + throw new Error('Either pass path or texture') + } const loadPath = (blockData ? 'blocks' : path ?? texture)! if (loadedImagesCache.has(loadPath)) { onLoad() @@ -201,6 +203,11 @@ const itemToVisualKey = (slot: RenderItem | Item | null) => { ].join('|') return keys } +const validateSlot = (slot: any, index: number) => { + if (!slot.texture) { + throw new Error(`Slot has no texture: ${index} ${slot.name}`) + } +} const mapSlots = (slots: Array<RenderItem | Item | null>, isJei = false) => { const newSlots = slots.map((slot, i) => { if (!slot) return null @@ -210,6 +217,7 @@ const mapSlots = (slots: Array<RenderItem | Item | null>, isJei = false) => { const newKey = itemToVisualKey(slot) slot['cacheKey'] = i + '|' + newKey if (oldKey && oldKey === newKey) { + validateSlot(lastMappedSlots[i], i) return lastMappedSlots[i] } } @@ -228,12 +236,13 @@ const mapSlots = (slots: Array<RenderItem | Item | null>, isJei = false) => { const { icon, ...rest } = slot return rest } + validateSlot(slot, i) } catch (err) { inGameError(err) } return slot }) - lastMappedSlots = newSlots + lastMappedSlots = JSON.parse(JSON.stringify(newSlots)) return newSlots } From 34eecc166f9a39cc5438b649e9c972e51c0490ee Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Sun, 29 Jun 2025 02:38:19 +0300 Subject: [PATCH 055/181] feat: rework singleplayer generators types. now any generator can be used internally. add a few --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- src/loadSave.ts | 21 --------------------- src/react/CreateWorld.tsx | 2 +- src/react/CreateWorldProvider.tsx | 22 +++------------------- src/sounds/botSoundSystem.ts | 1 + 6 files changed, 11 insertions(+), 47 deletions(-) diff --git a/package.json b/package.json index 25cce1a5..516d57f3 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "esbuild-plugin-polyfill-node": "^0.3.0", "express": "^4.18.2", "filesize": "^10.0.12", - "flying-squid": "npm:@zardoy/flying-squid@^0.0.59", + "flying-squid": "npm:@zardoy/flying-squid@^0.0.62", "framer-motion": "^12.9.2", "fs-extra": "^11.1.1", "google-drive-browserfs": "github:zardoy/browserfs#google-drive", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a034c919..f954791a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -117,8 +117,8 @@ importers: specifier: ^10.0.12 version: 10.1.6 flying-squid: - specifier: npm:@zardoy/flying-squid@^0.0.59 - version: '@zardoy/flying-squid@0.0.59(encoding@0.1.13)' + specifier: npm:@zardoy/flying-squid@^0.0.62 + version: '@zardoy/flying-squid@0.0.62(encoding@0.1.13)' framer-motion: specifier: ^12.9.2 version: 12.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -3410,8 +3410,8 @@ packages: engines: {node: '>=8'} hasBin: true - '@zardoy/flying-squid@0.0.59': - resolution: {integrity: sha512-Ztrmv127csGovqJEWEtT19y1wGEB5tIVfneQ3+p/TirP/bTGYpLlW+Ns4sSAc4KrewUP9PW/6L0AtB69CWhQFQ==} + '@zardoy/flying-squid@0.0.62': + resolution: {integrity: sha512-M6icydO/yrmwevBhmgKcqEPC63AhWfU/Es9N/uadVrmKaxGm2FQMMLcybbutRYm1xZ6qsdxDUOUZnN56PsVwfQ==} engines: {node: '>=8'} hasBin: true @@ -13249,7 +13249,7 @@ snapshots: - encoding - supports-color - '@zardoy/flying-squid@0.0.59(encoding@0.1.13)': + '@zardoy/flying-squid@0.0.62(encoding@0.1.13)': dependencies: '@tootallnate/once': 2.0.0 chalk: 5.4.1 diff --git a/src/loadSave.ts b/src/loadSave.ts index 7c9f7277..f1676cff 100644 --- a/src/loadSave.ts +++ b/src/loadSave.ts @@ -85,7 +85,6 @@ export const loadSave = async (root = '/world', connectOptions?: Partial<Connect } let version: string | undefined | null - let isFlat = false if (levelDat) { version = appQueryParams.mapVersion ?? levelDat.Version?.Name if (!version) { @@ -103,21 +102,6 @@ export const loadSave = async (root = '/world', connectOptions?: Partial<Connect version = prompt(`Version ${version} is not supported, supported versions are ${supportedVersions.join(', ')}, what try to use instead?`, lowerBound ? firstSupportedVersion : lastSupportedVersion) if (!version) return } - if (levelDat.WorldGenSettings) { - for (const [key, value] of Object.entries(levelDat.WorldGenSettings.dimensions)) { - if (key.slice(10) === 'overworld') { - if (value.generator.type === 'flat') isFlat = true - break - } - } - } - - if (levelDat.generatorName) { - isFlat = levelDat.generatorName === 'flat' - } - if (!isFlat && levelDat.generatorName !== 'default' && levelDat.generatorName !== 'customized') { - // warnings.push(`Generator ${levelDat.generatorName} may not be supported yet, be careful of new chunks writes`) - } const playerUuid = nameToMcOfflineUUID(options.localUsername) const playerDatPath = `${root}/playerdata/${playerUuid}.dat` @@ -188,11 +172,6 @@ export const loadSave = async (root = '/world', connectOptions?: Partial<Connect // todo check gamemode level.dat data etc detail: { version, - ...isFlat ? { - generation: { - name: 'superflat' - } - } : {}, ...root === '/world' ? {} : { 'worldFolder': root }, diff --git a/src/react/CreateWorld.tsx b/src/react/CreateWorld.tsx index 32e67ddb..4cdf186d 100644 --- a/src/react/CreateWorld.tsx +++ b/src/react/CreateWorld.tsx @@ -11,7 +11,7 @@ import styles from './createWorld.module.css' import { InputOption, showInputsModal, showOptionsModal } from './SelectOption' // const worldTypes = ['default', 'flat', 'largeBiomes', 'amplified', 'customized', 'buffet', 'debug_all_block_states'] -const worldTypes = ['default', 'flat'/* , 'void' */] +const worldTypes = ['default', 'flat', 'empty', 'nether', 'all_the_blocks'] const gameModes = ['survival', 'creative'/* , 'adventure', 'spectator' */] export const creatingWorldState = proxy({ diff --git a/src/react/CreateWorldProvider.tsx b/src/react/CreateWorldProvider.tsx index 6872474d..619a31f5 100644 --- a/src/react/CreateWorldProvider.tsx +++ b/src/react/CreateWorldProvider.tsx @@ -32,30 +32,14 @@ export default () => { const savePath = await uniqueFileNameFromWorldName(title, getWorldsPath()) await mkdirRecursive(savePath) await loadPluginsIntoWorld(savePath, plugins) - let generation - if (type === 'flat') { - generation = { - name: 'superflat', - } - } - if (type === 'void') { - generation = { - name: 'superflat', - layers: [], - noDefaults: true - } - } - if (type === 'nether') { - generation = { - name: 'nether' - } - } hideCurrentModal() window.dispatchEvent(new CustomEvent('singleplayer', { detail: { levelName: title, version, - generation, + generation: { + name: type + }, 'worldFolder': savePath, gameMode: gameMode === 'survival' ? 0 : 1, }, diff --git a/src/sounds/botSoundSystem.ts b/src/sounds/botSoundSystem.ts index 225aa345..0e23a98a 100644 --- a/src/sounds/botSoundSystem.ts +++ b/src/sounds/botSoundSystem.ts @@ -136,6 +136,7 @@ subscribeKey(miscUiState, 'gameLoaded', async () => { let lastStepSound = 0 const movementHappening = async () => { if (!bot.entity || !soundMap) return // no info yet + if (appViewer.playerState.reactive.gameMode === 'spectator') return // Don't play step sounds in spectator mode const VELOCITY_THRESHOLD = 0.1 const RUN_THRESHOLD = 0.15 const { x, z, y } = bot.entity.velocity From 6eae7136ecc9251ea41510a5b101d46372bad0a2 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Sun, 29 Jun 2025 15:34:25 +0300 Subject: [PATCH 056/181] fix storage conflict modal --- src/react/StorageConflictModal.tsx | 6 +++++- src/react/appStorageProvider.ts | 9 ++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/react/StorageConflictModal.tsx b/src/react/StorageConflictModal.tsx index ac78d90a..85affa7a 100644 --- a/src/react/StorageConflictModal.tsx +++ b/src/react/StorageConflictModal.tsx @@ -16,10 +16,14 @@ export default () => { if (!isModalActive/* || conflicts.length === 0 */) return null + const clampText = (text: string) => { + return text.length > 30 ? text.slice(0, 30) + '...' : text + } + const conflictText = conflicts.map(conflict => { const localTime = formatTimestamp(conflict.localStorageTimestamp) const cookieTime = formatTimestamp(conflict.cookieTimestamp) - return `${conflict.key}: LocalStorage (${localTime}) vs Cookie (${cookieTime})` + return `${conflict.key}: LocalStorage (${localTime}, ${clampText(conflict.localStorageValue)}) vs Cookie (${cookieTime}, ${clampText(conflict.cookieValue)})` }).join('\n') return ( diff --git a/src/react/appStorageProvider.ts b/src/react/appStorageProvider.ts index c65cc701..e36d43cf 100644 --- a/src/react/appStorageProvider.ts +++ b/src/react/appStorageProvider.ts @@ -134,7 +134,14 @@ const detectStorageConflicts = (): StorageConflict[] => { delete localData.timestamp delete cookieData.timestamp - if (JSON.stringify(localData) !== JSON.stringify(cookieData)) { + const isDataEmpty = (data: any) => { + if (typeof data === 'object' && data !== null) { + return Object.keys(data).length === 0 + } + return !data && data !== 0 && data !== false + } + + if (JSON.stringify(localData) !== JSON.stringify(cookieData) && !isDataEmpty(localData) && !isDataEmpty(cookieData)) { conflicts.push({ key, localStorageValue: localData, From f2a11d0a73ab42de75509e76edc004483adb0a34 Mon Sep 17 00:00:00 2001 From: Vitaly <vital2580@icloud.com> Date: Sun, 29 Jun 2025 20:44:58 +0300 Subject: [PATCH 057/181] Steingify if needed --- src/react/StorageConflictModal.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/react/StorageConflictModal.tsx b/src/react/StorageConflictModal.tsx index 85affa7a..966292d5 100644 --- a/src/react/StorageConflictModal.tsx +++ b/src/react/StorageConflictModal.tsx @@ -17,6 +17,7 @@ export default () => { if (!isModalActive/* || conflicts.length === 0 */) return null const clampText = (text: string) => { + if (typeof text !== "string") text = JSON.stringify(text) return text.length > 30 ? text.slice(0, 30) + '...' : text } From 31b91e5a335383ffe95838a038fb194d4f47288a Mon Sep 17 00:00:00 2001 From: Vitaly <vital2580@icloud.com> Date: Mon, 30 Jun 2025 02:06:45 +0300 Subject: [PATCH 058/181] add logging to sw unregistration on error --- index.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index 5a6318ec..371e22b1 100644 --- a/index.html +++ b/index.html @@ -49,9 +49,13 @@ if (window.navigator.maxTouchPoints > 1) window.location.hash = '#dev' // show eruda // unregister all sw if (window.navigator.serviceWorker) { + console.log('got worker') window.navigator.serviceWorker.getRegistrations().then(registrations => { registrations.forEach(registration => { - registration.unregister() + console.log('got registration') + registration.unregister().then(() => { + console.log('worker unregistered') + }) }) }) } From dc2c5a2d88c3ba006645331b4603e8585e811866 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Mon, 30 Jun 2025 19:11:59 +0300 Subject: [PATCH 059/181] fix: make it run on ios 15! --- index.html | 15 ++++++++++++ patches/pixelarticons@1.8.1.patch | 7 +++--- pnpm-lock.yaml | 38 +++++++++++++++--------------- renderer/viewer/lib/utils/skins.ts | 14 +++++++++-- renderer/viewer/three/entities.ts | 6 ++--- rsbuild.config.ts | 16 +++++++++++++ src/react/StorageConflictModal.tsx | 2 +- 7 files changed, 70 insertions(+), 28 deletions(-) diff --git a/index.html b/index.html index 371e22b1..6471c495 100644 --- a/index.html +++ b/index.html @@ -27,6 +27,7 @@ <div style="font-size: var(--font-size);color: rgb(176, 176, 176);margin-top: 3px;text-align: center" class="subtitle">A true Minecraft client in your browser!</div> <!-- small text pre --> <div style="font-size: calc(var(--font-size) * 0.6);color: rgb(150, 150, 150);margin-top: 3px;text-align: center;white-space: pre-line;" class="advanced-info"></div> + <div style="font-size: calc(var(--font-size) * 0.6);color: rgb(255, 100, 100);margin-top: 10px;text-align: center;display: none;" class="ios-warning">Only iOS 15+ is supported due to performance optimizations</div> </div> </div> ` @@ -36,6 +37,13 @@ if (!window.pageLoaded) { document.documentElement.appendChild(loadingDivElem) } + + // iOS version detection + const getIOSVersion = () => { + const match = navigator.userAgent.match(/OS (\d+)_(\d+)_?(\d+)?/); + return match ? parseInt(match[1], 10) : null; + } + // load error handling const onError = (errorOrMessage, log = false) => { let message = errorOrMessage instanceof Error ? (errorOrMessage.stack ?? errorOrMessage.message) : errorOrMessage @@ -46,6 +54,13 @@ const [errorMessage, ...errorStack] = message.split('\n') document.querySelector('.initial-loader').querySelector('.subtitle').textContent = errorMessage document.querySelector('.initial-loader').querySelector('.advanced-info').textContent = errorStack.join('\n') + + // Show iOS warning if applicable + const iosVersion = getIOSVersion(); + if (iosVersion !== null && iosVersion < 15) { + document.querySelector('.initial-loader').querySelector('.ios-warning').style.display = 'block'; + } + if (window.navigator.maxTouchPoints > 1) window.location.hash = '#dev' // show eruda // unregister all sw if (window.navigator.serviceWorker) { diff --git a/patches/pixelarticons@1.8.1.patch b/patches/pixelarticons@1.8.1.patch index 10044536..b65b6f2b 100644 --- a/patches/pixelarticons@1.8.1.patch +++ b/patches/pixelarticons@1.8.1.patch @@ -1,5 +1,5 @@ diff --git a/fonts/pixelart-icons-font.css b/fonts/pixelart-icons-font.css -index 3b2ebe839370d96bf93ef5ca94a827f07e49378d..103ab4d6b9f3b5c9f41d1407e3cbf4ac392fbf41 100644 +index 3b2ebe839370d96bf93ef5ca94a827f07e49378d..4f8d76be2ca6e4ddc43c68d0a6f0f69979165ab4 100644 --- a/fonts/pixelart-icons-font.css +++ b/fonts/pixelart-icons-font.css @@ -1,16 +1,13 @@ @@ -10,10 +10,11 @@ index 3b2ebe839370d96bf93ef5ca94a827f07e49378d..103ab4d6b9f3b5c9f41d1407e3cbf4ac + src: url("pixelart-icons-font.woff2?t=1711815892278") format("woff2"), url("pixelart-icons-font.woff?t=1711815892278") format("woff"), - url('pixelart-icons-font.ttf?t=1711815892278') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ +- url('pixelart-icons-font.ttf?t=1711815892278') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ - url('pixelart-icons-font.svg?t=1711815892278#pixelart-icons-font') format('svg'); /* iOS 4.1- */ ++ url('pixelart-icons-font.ttf?t=1711815892278') format('truetype'); /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ } - + [class^="pixelart-icons-font-"], [class*=" pixelart-icons-font-"] { font-family: 'pixelart-icons-font' !important; - font-size:24px; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f954791a..2a46908a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,7 +28,7 @@ patchedDependencies: hash: a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad path: patches/mineflayer-item-map-downloader@1.2.0.patch pixelarticons@1.8.1: - hash: d6a3d784047beba873565d1198bed425d9eb2de942e3fc8edac55f25473e4325 + hash: 533230072bc402f425c86abd3d0356fe087b14cab2a254d93f419b083f2d8dfa path: patches/pixelarticons@1.8.1.patch importers: @@ -136,13 +136,13 @@ importers: version: 4.17.21 mcraft-fun-mineflayer: specifier: ^0.1.23 - version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/0380bed150fe03db4ac37f0194e0cee98356647d(encoding@0.1.13)) + version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/67db1808c38fab66d77ccf7822ff7c4f59308470(encoding@0.1.13)) minecraft-data: specifier: 3.89.0 version: 3.89.0 minecraft-protocol: specifier: github:PrismarineJS/node-minecraft-protocol#master - version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) + version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/1e38d8fc1e1bbe7aa834055cfd38ed0fa22c2085(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) mineflayer-item-map-downloader: specifier: github:zardoy/mineflayer-item-map-downloader version: https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad)(encoding@0.1.13) @@ -160,7 +160,7 @@ importers: version: 1.5.4 pixelarticons: specifier: ^1.8.1 - version: 1.8.1(patch_hash=d6a3d784047beba873565d1198bed425d9eb2de942e3fc8edac55f25473e4325) + version: 1.8.1(patch_hash=533230072bc402f425c86abd3d0356fe087b14cab2a254d93f419b083f2d8dfa) pretty-bytes: specifier: ^6.1.1 version: 6.1.1 @@ -341,7 +341,7 @@ importers: version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/5494f356b1e59eddc876c3ae05ff395f12a46379(@types/react@18.3.18)(react@18.3.1) mineflayer: specifier: github:zardoy/mineflayer#gen-the-master - version: https://codeload.github.com/zardoy/mineflayer/tar.gz/0380bed150fe03db4ac37f0194e0cee98356647d(encoding@0.1.13) + version: https://codeload.github.com/zardoy/mineflayer/tar.gz/67db1808c38fab66d77ccf7822ff7c4f59308470(encoding@0.1.13) mineflayer-mouse: specifier: ^0.1.10 version: 0.1.10(@types/debug@4.1.12)(@types/node@22.13.9)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) @@ -6709,8 +6709,8 @@ packages: resolution: {tarball: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/5494f356b1e59eddc876c3ae05ff395f12a46379} version: 1.0.1 - minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284: - resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284} + minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/1e38d8fc1e1bbe7aa834055cfd38ed0fa22c2085: + resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/1e38d8fc1e1bbe7aa834055cfd38ed0fa22c2085} version: 1.57.0 engines: {node: '>=22'} @@ -6736,8 +6736,8 @@ packages: resolution: {integrity: sha512-3bxph4jfbkBh5HpeouorxzrfSLNV+i+1gugNJ2jf52HW+rt+tW7eiiFPxrJEsOVkPT/3O/dEIW7j93LRlojMkQ==} engines: {node: '>=22'} - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/0380bed150fe03db4ac37f0194e0cee98356647d: - resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/0380bed150fe03db4ac37f0194e0cee98356647d} + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/67db1808c38fab66d77ccf7822ff7c4f59308470: + resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/67db1808c38fab66d77ccf7822ff7c4f59308470} version: 4.27.0 engines: {node: '>=22'} @@ -13224,7 +13224,7 @@ snapshots: flatmap: 0.0.3 long: 5.3.1 minecraft-data: 3.89.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/1e38d8fc1e1bbe7aa834055cfd38ed0fa22c2085(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 @@ -13260,7 +13260,7 @@ snapshots: flatmap: 0.0.3 long: 5.3.1 minecraft-data: 3.89.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/1e38d8fc1e1bbe7aa834055cfd38ed0fa22c2085(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 @@ -17127,12 +17127,12 @@ snapshots: maxrects-packer: '@zardoy/maxrects-packer@2.7.4' zod: 3.24.2 - mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/0380bed150fe03db4ac37f0194e0cee98356647d(encoding@0.1.13)): + mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/67db1808c38fab66d77ccf7822ff7c4f59308470(encoding@0.1.13)): dependencies: '@zardoy/flying-squid': 0.0.49(encoding@0.1.13) exit-hook: 2.2.1 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) - mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/0380bed150fe03db4ac37f0194e0cee98356647d(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/1e38d8fc1e1bbe7aa834055cfd38ed0fa22c2085(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) + mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/67db1808c38fab66d77ccf7822ff7c4f59308470(encoding@0.1.13) prismarine-item: 1.16.0 ws: 8.18.1 transitivePeerDependencies: @@ -17450,7 +17450,7 @@ snapshots: - '@types/react' - react - minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13): + minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/1e38d8fc1e1bbe7aa834055cfd38ed0fa22c2085(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13): dependencies: '@types/node-rsa': 1.1.4 '@types/readable-stream': 4.0.18 @@ -17549,7 +17549,7 @@ snapshots: mineflayer@4.27.0(encoding@0.1.13): dependencies: minecraft-data: 3.89.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/1e38d8fc1e1bbe7aa834055cfd38ed0fa22c2085(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) prismarine-biome: 1.3.0(minecraft-data@3.89.0)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chat: 1.11.0 @@ -17569,11 +17569,11 @@ snapshots: - encoding - supports-color - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/0380bed150fe03db4ac37f0194e0cee98356647d(encoding@0.1.13): + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/67db1808c38fab66d77ccf7822ff7c4f59308470(encoding@0.1.13): dependencies: '@nxg-org/mineflayer-physics-util': 1.8.10 minecraft-data: 3.89.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/1e38d8fc1e1bbe7aa834055cfd38ed0fa22c2085(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) prismarine-biome: 1.3.0(minecraft-data@3.89.0)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chat: 1.11.0 @@ -18263,7 +18263,7 @@ snapshots: pirates@4.0.6: {} - pixelarticons@1.8.1(patch_hash=d6a3d784047beba873565d1198bed425d9eb2de942e3fc8edac55f25473e4325): {} + pixelarticons@1.8.1(patch_hash=533230072bc402f425c86abd3d0356fe087b14cab2a254d93f419b083f2d8dfa): {} pixelmatch@4.0.2: dependencies: diff --git a/renderer/viewer/lib/utils/skins.ts b/renderer/viewer/lib/utils/skins.ts index e0c8c32e..de5a3675 100644 --- a/renderer/viewer/lib/utils/skins.ts +++ b/renderer/viewer/lib/utils/skins.ts @@ -16,12 +16,22 @@ export const loadThreeJsTextureFromUrlSync = (imageUrl: string) => { } } +export const createCanvas = (width: number, height: number): OffscreenCanvas => { + if (typeof OffscreenCanvas !== 'undefined') { + return new OffscreenCanvas(width, height) + } + const canvas = document.createElement('canvas') + canvas.width = width + canvas.height = height + return canvas as unknown as OffscreenCanvas // todo-low +} + export const loadThreeJsTextureFromUrl = async (imageUrl: string) => { const loaded = new THREE.TextureLoader().loadAsync(imageUrl) return loaded } export const loadThreeJsTextureFromBitmap = (image: ImageBitmap) => { - const canvas = new OffscreenCanvas(image.width, image.height) + const canvas = createCanvas(image.width, image.height) const ctx = canvas.getContext('2d')! ctx.drawImage(image, 0, 0) const texture = new THREE.Texture(canvas) @@ -83,7 +93,7 @@ export async function loadSkinImage (skinUrl: string): Promise<{ canvas: Offscre } const image = await loadImageFromUrl(skinUrl) - const skinCanvas = new OffscreenCanvas(64, 64) + const skinCanvas = createCanvas(64, 64) loadSkinToCanvas(skinCanvas, image) return { canvas: skinCanvas, image } } diff --git a/renderer/viewer/three/entities.ts b/renderer/viewer/three/entities.ts index b4de4f1b..3e37102c 100644 --- a/renderer/viewer/three/entities.ts +++ b/renderer/viewer/three/entities.ts @@ -18,7 +18,7 @@ import { isEntityAttackable } from 'mineflayer-mouse/dist/attackableEntity' import { Vec3 } from 'vec3' import { EntityMetadataVersions } from '../../../src/mcDataTypes' import { ItemSpecificContextProperties } from '../lib/basePlayerState' -import { loadSkinImage, loadSkinFromUsername, stevePngUrl, steveTexture } from '../lib/utils/skins' +import { loadSkinImage, loadSkinFromUsername, stevePngUrl, steveTexture, createCanvas } from '../lib/utils/skins' import { loadTexture } from '../lib/utils' import { getBlockMeshFromModel } from './holdingBlock' import * as Entity from './entity/EntityMesh' @@ -96,7 +96,7 @@ function getUsernameTexture ({ nameTagBackgroundColor = 'rgba(0, 0, 0, 0.3)', nameTagTextOpacity = 255 }: any, { fontFamily = 'sans-serif' }: any) { - const canvas = new OffscreenCanvas(64, 64) + const canvas = createCanvas(64, 64) const ctx = canvas.getContext('2d') if (!ctx) throw new Error('Could not get 2d context') @@ -550,7 +550,7 @@ export class Entities { let skinCanvas: OffscreenCanvas if (skinUrl === stevePngUrl) { skinTexture = await steveTexture - const canvas = new OffscreenCanvas(64, 64) + const canvas = createCanvas(64, 64) const ctx = canvas.getContext('2d') if (!ctx) throw new Error('Failed to get context') ctx.drawImage(skinTexture.image, 0, 0) diff --git a/rsbuild.config.ts b/rsbuild.config.ts index 548be4e2..f7cbfaac 100644 --- a/rsbuild.config.ts +++ b/rsbuild.config.ts @@ -111,6 +111,22 @@ const appConfig = defineConfig({ js: 'source-map', css: true, }, + minify: { + // js: false, + jsOptions: { + minimizerOptions: { + mangle: { + safari10: true, + keep_classnames: true, + keep_fnames: true, + keep_private_props: true, + }, + compress: { + unused: true, + }, + }, + }, + }, distPath: SINGLE_FILE_BUILD ? { html: './single', } : undefined, diff --git a/src/react/StorageConflictModal.tsx b/src/react/StorageConflictModal.tsx index 966292d5..e1d3299d 100644 --- a/src/react/StorageConflictModal.tsx +++ b/src/react/StorageConflictModal.tsx @@ -17,7 +17,7 @@ export default () => { if (!isModalActive/* || conflicts.length === 0 */) return null const clampText = (text: string) => { - if (typeof text !== "string") text = JSON.stringify(text) + if (typeof text !== 'string') text = JSON.stringify(text) return text.length > 30 ? text.slice(0, 30) + '...' : text } From 1310109c016c5911330abae186008237ae1bf025 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Mon, 30 Jun 2025 19:48:43 +0300 Subject: [PATCH 060/181] fix: username was not saved after properly after resolving the storage conflict --- rsbuild.config.ts | 1 + src/react/appStorageProvider.ts | 13 ++++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/rsbuild.config.ts b/rsbuild.config.ts index f7cbfaac..8bcbd75d 100644 --- a/rsbuild.config.ts +++ b/rsbuild.config.ts @@ -157,6 +157,7 @@ const appConfig = defineConfig({ 'process.env.DISABLE_SERVICE_WORKER': JSON.stringify(disableServiceWorker), 'process.env.INLINED_APP_CONFIG': JSON.stringify(configSource === 'BUNDLED' ? configJson : null), 'process.env.ENABLE_COOKIE_STORAGE': JSON.stringify(process.env.ENABLE_COOKIE_STORAGE || true), + 'process.env.COOKIE_STORAGE_PREFIX': JSON.stringify(process.env.COOKIE_STORAGE_PREFIX || ''), }, }, server: { diff --git a/src/react/appStorageProvider.ts b/src/react/appStorageProvider.ts index e36d43cf..bce6feca 100644 --- a/src/react/appStorageProvider.ts +++ b/src/react/appStorageProvider.ts @@ -8,7 +8,7 @@ import type { BaseServerInfo } from './AddServerOrConnect' // when opening html file locally in browser, localStorage is shared between all ever opened html files, so we try to avoid conflicts const localStoragePrefix = process.env?.SINGLE_FILE_BUILD ? 'minecraft-web-client:' : '' -const cookiePrefix = '' +const cookiePrefix = process.env.COOKIE_STORAGE_PREFIX || '' const { localStorage } = window const migrateRemoveLocalStorage = false @@ -146,7 +146,7 @@ const detectStorageConflicts = (): StorageConflict[] => { key, localStorageValue: localData, localStorageTimestamp: localTimestamp, - cookieValue: cookieData, + cookieValue: (typeof cookieData === 'object' && cookieData !== null && 'data' in cookieData) ? cookieData.data : cookieData, cookieTimestamp }) } @@ -231,9 +231,10 @@ export const appStorage = proxy({ ...defaultStorageData }) // Check if cookie storage should be used (will be set by options) const shouldUseCookieStorage = () => { + const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) const isSecureCookiesAvailable = () => { // either https or localhost - return window.location.protocol === 'https:' || window.location.hostname === 'localhost' + return window.location.protocol === 'https:' || (window.location.hostname === 'localhost' && !isSafari) } if (!isSecureCookiesAvailable()) { return false @@ -402,6 +403,12 @@ export const resolveStorageConflicts = (useLocalStorage: boolean) => { } } + // forcefully set data again + for (const conflict of storageConflicts) { + appStorage[conflict.key] = useLocalStorage ? conflict.localStorageValue : conflict.cookieValue + saveKey(conflict.key as keyof StorageData) + } + // Clear conflicts and restore data storageConflicts = [] restoreStorageData() From d4ff7de64ede876703bce76b254124c9f929bccf Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Mon, 30 Jun 2025 19:53:09 +0300 Subject: [PATCH 061/181] stop building storybook... --- .github/workflows/ci.yml | 2 +- .github/workflows/preview.yml | 1 - .github/workflows/release.yml | 5 ++--- README.MD | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc344aad..8fc56ea9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: cd package zip -r ../self-host.zip . - run: pnpm build-playground - - run: pnpm build-storybook + # - run: pnpm build-storybook - run: pnpm test-unit - run: pnpm lint diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index f35e6a00..f243a395 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -78,7 +78,6 @@ jobs: run: vercel build --token=${{ secrets.VERCEL_TOKEN }} env: CONFIG_JSON_SOURCE: BUNDLED - - run: pnpm build-storybook - name: Copy playground files run: | mkdir -p .vercel/output/static/playground diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 14c4b471..76322cb5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,7 +34,6 @@ jobs: - run: vercel build --token=${{ secrets.VERCEL_TOKEN }} --prod env: CONFIG_JSON_SOURCE: BUNDLED - - run: pnpm build-storybook - name: Copy playground files run: | mkdir -p .vercel/output/static/playground @@ -48,12 +47,12 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: .vercel/output/static force_orphan: true - + - name: Change index.html title run: | # change <title>Minecraft Web Client to Minecraft Web Client — Free Online Browser Version sed -i 's/Minecraft Web Client<\/title>/<title>Minecraft Web Client — Free Online Browser Version<\/title>/' .vercel/output/static/index.html - + - name: Deploy Project to Vercel uses: mathiasvr/command-output@v2.0.0 with: diff --git a/README.MD b/README.MD index 5f7a83f6..281d2539 100644 --- a/README.MD +++ b/README.MD @@ -30,7 +30,7 @@ For building the project yourself / contributing, see [Development, Debugging & - Support for custom rendering 3D engines. Modular architecture. - even even more! -All components that are in [Storybook](https://mcraft.fun/storybook) are published as npm module and can be used in other projects: [`minecraft-react`](https://npmjs.com/minecraft-react) +All components that are in [Storybook](https://minimap.mcraft.fun/storybook/) are published as npm module and can be used in other projects: [`minecraft-react`](https://npmjs.com/minecraft-react) ### Recommended Settings From 13e145cc3ad4ef2b2501ad79624e0fcac9e6f120 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Tue, 1 Jul 2025 00:05:43 +0300 Subject: [PATCH 062/181] docs: add one liner script! --- README.MD | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.MD b/README.MD index 281d2539..ea5a1453 100644 --- a/README.MD +++ b/README.MD @@ -12,6 +12,9 @@ Don't confuse with [Eaglercraft](https://git.eaglercraft.rip/eaglercraft/eaglerc For building the project yourself / contributing, see [Development, Debugging & Contributing](#development-debugging--contributing). For reference at what and how web technologies / frameworks are used, see [TECH.md](./TECH.md) (also for comparison with Eaglercraft). +> **Note**: You can deploy it on your own server in less than a minute using a one-liner script from [Minecraft Everywhere repo](https://github.com/zardoy/minecraft-everywhere) + + ### Big Features - Official Mineflayer [plugin integration](https://github.com/zardoy/mcraft-fun-mineflayer-plugin)! View / Control your bot remotely. From 6f5239e1d81709a5284c395dbeac18aa3570604f Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Tue, 1 Jul 2025 02:29:37 +0300 Subject: [PATCH 063/181] fix: fix inventory crash on picking item with gui-generated texture, fix shulker box --- renderer/viewer/three/renderSlot.ts | 2 -- src/inventoryWindows.ts | 15 ++++++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/renderer/viewer/three/renderSlot.ts b/renderer/viewer/three/renderSlot.ts index fd1eae91..d82e58e3 100644 --- a/renderer/viewer/three/renderSlot.ts +++ b/renderer/viewer/three/renderSlot.ts @@ -14,7 +14,6 @@ export const renderSlot = (model: ResolvedItemModelRender, resourcesManager: Res scale?: number, slice?: number[], modelName?: string, - image?: ImageBitmap } | undefined => { let itemModelName = model.modelName const isItem = loadedData.itemsByName[itemModelName] @@ -36,7 +35,6 @@ export const renderSlot = (model: ResolvedItemModelRender, resourcesManager: Res const y = item.v * atlas.height return { texture: 'gui', - image: resourcesManager.currentResources!.guiAtlas!.image, slice: [x, y, atlas.tileSize, atlas.tileSize], scale: 0.25, } diff --git a/src/inventoryWindows.ts b/src/inventoryWindows.ts index 01d1691d..c9f60d59 100644 --- a/src/inventoryWindows.ts +++ b/src/inventoryWindows.ts @@ -23,7 +23,7 @@ import { MessageFormatPart } from './chatUtils' import { GeneralInputItem, getItemMetadata, getItemModelName, getItemNameRaw, RenderItem } from './mineflayer/items' import { playerState } from './mineflayer/playerState' -const loadedImagesCache = new Map<string, HTMLImageElement>() +const loadedImagesCache = new Map<string, HTMLImageElement | ImageBitmap>() const cleanLoadedImagesCache = () => { loadedImagesCache.delete('blocks') loadedImagesCache.delete('items') @@ -132,11 +132,12 @@ export const onGameLoad = () => { } } -const getImageSrc = (path): string | HTMLImageElement => { +const getImageSrc = (path): string | HTMLImageElement | ImageBitmap => { switch (path) { case 'gui/container/inventory': return appReplacableResources.latest_gui_container_inventory.content case 'blocks': return appViewer.resourcesManager.blocksAtlasParser.latestImage case 'items': return appViewer.resourcesManager.itemsAtlasParser.latestImage + case 'gui': return appViewer.resourcesManager.currentResources!.guiAtlas!.image case 'gui/container/dispenser': return appReplacableResources.latest_gui_container_dispenser.content case 'gui/container/furnace': return appReplacableResources.latest_gui_container_furnace.content case 'gui/container/crafting_table': return appReplacableResources.latest_gui_container_crafting_table.content @@ -167,6 +168,12 @@ const getImage = ({ path = undefined as string | undefined, texture = undefined onLoad() } else { const imageSrc = getImageSrc(loadPath) + if (imageSrc instanceof ImageBitmap) { + onLoad() + loadedImagesCache.set(loadPath, imageSrc) + return imageSrc + } + let image: HTMLImageElement if (imageSrc instanceof Image) { image = imageSrc @@ -277,6 +284,7 @@ const implementedContainersGuiMap = { 'minecraft:generic_3x3': 'DropDispenseWin', 'minecraft:furnace': 'FurnaceWin', 'minecraft:smoker': 'FurnaceWin', + 'minecraft:shulker_box': 'ChestWin', 'minecraft:blast_furnace': 'FurnaceWin', 'minecraft:crafting': 'CraftingWin', 'minecraft:crafting3x3': 'CraftingWin', // todo different result slot @@ -367,7 +375,7 @@ const openWindow = (type: string | undefined) => { lastWindow.destroy() lastWindow = null as any lastWindowType = null - window.lastWindow = lastWindow + window.inventory = null miscUiState.displaySearchInput = false destroyFn() skipClosePacketSending = false @@ -375,6 +383,7 @@ const openWindow = (type: string | undefined) => { cleanLoadedImagesCache() const inv = openItemsCanvas(type) inv.canvasManager.children[0].mobileHelpers = miscUiState.currentTouch + window.inventory = inv const title = bot.currentWindow?.title const PrismarineChat = PrismarineChatLoader(bot.version) try { From f79e54f11dfb1608c7c8ce2426bf3ab31c526342 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Wed, 2 Jul 2025 05:45:14 +0300 Subject: [PATCH 064/181] limit columns in player tab --- src/react/PlayerListOverlayProvider.tsx | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/react/PlayerListOverlayProvider.tsx b/src/react/PlayerListOverlayProvider.tsx index 3ff69c41..4d8a8ed7 100644 --- a/src/react/PlayerListOverlayProvider.tsx +++ b/src/react/PlayerListOverlayProvider.tsx @@ -5,6 +5,7 @@ import PlayerListOverlay from './PlayerListOverlay' import './PlayerListOverlay.css' import { lastConnectOptions } from './AppStatusProvider' +const MAX_COLUMNS = 4 const MAX_ROWS_PER_COL = 10 type Players = typeof bot.players @@ -56,21 +57,24 @@ export default () => { } }, [serverIp]) - const playersArray = Object.values(players).sort((a, b) => { if (a.username > b.username) return 1 if (a.username < b.username) return -1 return 0 }) + + // Calculate optimal column distribution + const totalPlayers = playersArray.length + const numColumns = Math.min(MAX_COLUMNS, Math.ceil(totalPlayers / MAX_ROWS_PER_COL)) + const playersPerColumn = Math.ceil(totalPlayers / numColumns) + const lists = [] as Array<typeof playersArray> - let tempList = [] as typeof playersArray - for (let i = 0; i < playersArray.length; i++) { - tempList.push(playersArray[i]) - - if ((i + 1) % MAX_ROWS_PER_COL === 0 || i + 1 === playersArray.length) { - lists.push([...tempList]) - tempList = [] + for (let i = 0; i < numColumns; i++) { + const startIdx = i * playersPerColumn + const endIdx = Math.min(startIdx + playersPerColumn, totalPlayers) + if (startIdx < totalPlayers) { + lists.push(playersArray.slice(startIdx, endIdx)) } } From a2711dbe6c58f6f59ef8c881193b1e2d69f3d8cb Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Wed, 2 Jul 2025 05:45:18 +0300 Subject: [PATCH 065/181] up mc-assets --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 516d57f3..8d40c477 100644 --- a/package.json +++ b/package.json @@ -150,7 +150,7 @@ "http-browserify": "^1.7.0", "http-server": "^14.1.1", "https-browserify": "^1.0.0", - "mc-assets": "^0.2.59", + "mc-assets": "^0.2.62", "minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next", "mineflayer": "github:zardoy/mineflayer#gen-the-master", "mineflayer-mouse": "^0.1.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a46908a..4d49692d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -334,8 +334,8 @@ importers: specifier: ^1.0.0 version: 1.0.0 mc-assets: - specifier: ^0.2.59 - version: 0.2.59 + specifier: ^0.2.62 + version: 0.2.62 minecraft-inventory-gui: specifier: github:zardoy/minecraft-inventory-gui#next version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/5494f356b1e59eddc876c3ae05ff395f12a46379(@types/react@18.3.18)(react@18.3.1) @@ -6487,8 +6487,8 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - mc-assets@0.2.59: - resolution: {integrity: sha512-HGdy6v09X5nks8+NuwrL3KQ763D+eWFeSpWLXx3+doWz6hSEeLjgBPOrB1stvQOjPDiQCzsIv5gaRB5sl6ng1A==} + mc-assets@0.2.62: + resolution: {integrity: sha512-RYZeD1+joNlPuUpi+tIWkbP0ieVJr+R6IFkI6/8juhSxx9zE4osoSmteybrfspGm8A6u+YbbY1epqRKEMwVR6Q==} engines: {node: '>=18.0.0'} mcraft-fun-mineflayer@0.1.23: @@ -17122,7 +17122,7 @@ snapshots: math-intrinsics@1.1.0: {} - mc-assets@0.2.59: + mc-assets@0.2.62: dependencies: maxrects-packer: '@zardoy/maxrects-packer@2.7.4' zod: 3.24.2 From c55827db962c60d8ab2f21189ee372c98113a188 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Wed, 2 Jul 2025 05:46:53 +0300 Subject: [PATCH 066/181] up mc-data --- package.json | 4 +-- pnpm-lock.yaml | 74 +++++++++++++++++++++++++------------------------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/package.json b/package.json index 8d40c477..eb53a8bf 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "jszip": "^3.10.1", "lodash-es": "^4.17.21", "mcraft-fun-mineflayer": "^0.1.23", - "minecraft-data": "3.89.0", + "minecraft-data": "3.92.0", "minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master", "mineflayer-item-map-downloader": "github:zardoy/mineflayer-item-map-downloader", "mojangson": "^2.0.4", @@ -201,7 +201,7 @@ "diamond-square": "github:zardoy/diamond-square", "prismarine-block": "github:zardoy/prismarine-block#next-era", "prismarine-world": "github:zardoy/prismarine-world#next-era", - "minecraft-data": "3.89.0", + "minecraft-data": "3.92.0", "prismarine-provider-anvil": "github:zardoy/prismarine-provider-anvil#everything", "prismarine-physics": "github:zardoy/prismarine-physics", "minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4d49692d..c904c3f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,7 +12,7 @@ overrides: diamond-square: github:zardoy/diamond-square prismarine-block: github:zardoy/prismarine-block#next-era prismarine-world: github:zardoy/prismarine-world#next-era - minecraft-data: 3.89.0 + minecraft-data: 3.92.0 prismarine-provider-anvil: github:zardoy/prismarine-provider-anvil#everything prismarine-physics: github:zardoy/prismarine-physics minecraft-protocol: github:PrismarineJS/node-minecraft-protocol#master @@ -138,8 +138,8 @@ importers: specifier: ^0.1.23 version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/67db1808c38fab66d77ccf7822ff7c4f59308470(encoding@0.1.13)) minecraft-data: - specifier: 3.89.0 - version: 3.89.0 + specifier: 3.92.0 + version: 3.92.0 minecraft-protocol: specifier: github:PrismarineJS/node-minecraft-protocol#master version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/1e38d8fc1e1bbe7aa834055cfd38ed0fa22c2085(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) @@ -166,7 +166,7 @@ importers: version: 6.1.1 prismarine-provider-anvil: specifier: github:zardoy/prismarine-provider-anvil#everything - version: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.89.0) + version: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.92.0) prosemirror-example-setup: specifier: ^1.2.2 version: 1.2.3 @@ -435,7 +435,7 @@ importers: version: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chunk: specifier: github:zardoy/prismarine-chunk#master - version: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.89.0) + version: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0) prismarine-schematic: specifier: ^1.2.0 version: 1.2.3 @@ -6699,8 +6699,8 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} - minecraft-data@3.89.0: - resolution: {integrity: sha512-v6dUr1M7Pjc6N4ujanrBZu3IP4/HbSBpxSSXNbK6HVFVJqfaqKSMXN57G/JAlDcwqXYsVd9H4tbKFHCO+VmQpg==} + minecraft-data@3.92.0: + resolution: {integrity: sha512-CGfO50svzm+pSRa4Mbq4owsmRKbPCNkSZ3MCOyH+epC7yNjh+PUhPQFHWq72O51qsY7pAB5qM/bJn1ncwG1J5g==} minecraft-folder-path@1.2.0: resolution: {integrity: sha512-qaUSbKWoOsH9brn0JQuBhxNAzTDMwrOXorwuRxdJKKKDYvZhtml+6GVCUrY5HRiEsieBEjCUnhVpDuQiKsiFaw==} @@ -7436,7 +7436,7 @@ packages: prismarine-biome@1.3.0: resolution: {integrity: sha512-GY6nZxq93mTErT7jD7jt8YS1aPrOakbJHh39seYsJFXvueIOdHAmW16kYQVrTVMW5MlWLQVxV/EquRwOgr4MnQ==} peerDependencies: - minecraft-data: 3.89.0 + minecraft-data: 3.92.0 prismarine-registry: ^1.1.0 prismarine-block@https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9: @@ -11422,7 +11422,7 @@ snapshots: '@nxg-org/mineflayer-trajectories@1.2.0(encoding@0.1.13)': dependencies: '@nxg-org/mineflayer-util-plugin': 1.8.4 - minecraft-data: 3.89.0 + minecraft-data: 3.92.0 mineflayer: 4.27.0(encoding@0.1.13) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-item: 1.16.0 @@ -13223,16 +13223,16 @@ snapshots: exit-hook: 2.2.1 flatmap: 0.0.3 long: 5.3.1 - minecraft-data: 3.89.0 + minecraft-data: 3.92.0 minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/1e38d8fc1e1bbe7aa834055cfd38ed0fa22c2085(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) 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/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.89.0) + prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0) prismarine-entity: 2.5.0 prismarine-item: 1.16.0 prismarine-nbt: 2.7.0 - prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.89.0) + prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.92.0) prismarine-windows: 2.9.0 prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c rambda: 9.4.2 @@ -13259,16 +13259,16 @@ snapshots: exit-hook: 2.2.1 flatmap: 0.0.3 long: 5.3.1 - minecraft-data: 3.89.0 + minecraft-data: 3.92.0 minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/1e38d8fc1e1bbe7aa834055cfd38ed0fa22c2085(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) 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/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.89.0) + prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0) prismarine-entity: 2.5.0 prismarine-item: 1.16.0 prismarine-nbt: 2.7.0 - prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.89.0) + prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.92.0) prismarine-windows: 2.9.0 prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c rambda: 9.4.2 @@ -14673,8 +14673,8 @@ snapshots: diamond-square@https://codeload.github.com/zardoy/diamond-square/tar.gz/cfaad2d1d5909fdfa63c8cc7bc05fb5e87782d71: dependencies: - minecraft-data: 3.89.0 - prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.89.0) + minecraft-data: 3.92.0 + prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0) prismarine-registry: 1.11.0 random-seed: 0.3.0 vec3: 0.1.10 @@ -17439,7 +17439,7 @@ snapshots: min-indent@1.0.1: {} - minecraft-data@3.89.0: {} + minecraft-data@3.92.0: {} minecraft-folder-path@1.2.0: {} @@ -17459,7 +17459,7 @@ snapshots: debug: 4.4.0(supports-color@8.1.1) endian-toggle: 0.0.0 lodash.merge: 4.6.2 - minecraft-data: 3.89.0 + minecraft-data: 3.92.0 minecraft-folder-path: 1.2.0 node-fetch: 2.7.0(encoding@0.1.13) node-rsa: 0.4.2 @@ -17538,7 +17538,7 @@ snapshots: mineflayer-pathfinder@2.4.5: dependencies: - minecraft-data: 3.89.0 + minecraft-data: 3.92.0 prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-entity: 2.5.0 prismarine-item: 1.16.0 @@ -17548,12 +17548,12 @@ snapshots: mineflayer@4.27.0(encoding@0.1.13): dependencies: - minecraft-data: 3.89.0 + minecraft-data: 3.92.0 minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/1e38d8fc1e1bbe7aa834055cfd38ed0fa22c2085(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) - prismarine-biome: 1.3.0(minecraft-data@3.89.0)(prismarine-registry@1.11.0) + prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chat: 1.11.0 - prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.89.0) + prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0) prismarine-entity: 2.5.0 prismarine-item: 1.16.0 prismarine-nbt: 2.7.0 @@ -17572,12 +17572,12 @@ snapshots: mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/67db1808c38fab66d77ccf7822ff7c4f59308470(encoding@0.1.13): dependencies: '@nxg-org/mineflayer-physics-util': 1.8.10 - minecraft-data: 3.89.0 + minecraft-data: 3.92.0 minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/1e38d8fc1e1bbe7aa834055cfd38ed0fa22c2085(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) - prismarine-biome: 1.3.0(minecraft-data@3.89.0)(prismarine-registry@1.11.0) + prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chat: 1.11.0 - prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.89.0) + prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0) prismarine-entity: 2.5.0 prismarine-item: 1.16.0 prismarine-nbt: 2.7.0 @@ -18363,15 +18363,15 @@ snapshots: transitivePeerDependencies: - supports-color - prismarine-biome@1.3.0(minecraft-data@3.89.0)(prismarine-registry@1.11.0): + prismarine-biome@1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0): dependencies: - minecraft-data: 3.89.0 + minecraft-data: 3.92.0 prismarine-registry: 1.11.0 prismarine-block@https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9: dependencies: - minecraft-data: 3.89.0 - prismarine-biome: 1.3.0(minecraft-data@3.89.0)(prismarine-registry@1.11.0) + minecraft-data: 3.92.0 + prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) prismarine-chat: 1.11.0 prismarine-item: 1.16.0 prismarine-nbt: 2.7.0 @@ -18383,9 +18383,9 @@ snapshots: prismarine-nbt: 2.7.0 prismarine-registry: 1.11.0 - prismarine-chunk@https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.89.0): + prismarine-chunk@https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0): dependencies: - prismarine-biome: 1.3.0(minecraft-data@3.89.0)(prismarine-registry@1.11.0) + prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-nbt: 2.7.0 prismarine-registry: 1.11.0 @@ -18414,14 +18414,14 @@ snapshots: prismarine-physics@https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b: dependencies: - minecraft-data: 3.89.0 + minecraft-data: 3.92.0 prismarine-nbt: 2.7.0 vec3: 0.1.10 - prismarine-provider-anvil@https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.89.0): + prismarine-provider-anvil@https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.92.0): dependencies: prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 - prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.89.0) + prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0) prismarine-nbt: 2.7.0 prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c uint4: 0.1.2 @@ -18443,13 +18443,13 @@ snapshots: prismarine-registry@1.11.0: dependencies: - minecraft-data: 3.89.0 + minecraft-data: 3.92.0 prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-nbt: 2.7.0 prismarine-schematic@1.2.3: dependencies: - minecraft-data: 3.89.0 + minecraft-data: 3.92.0 prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-nbt: 2.7.0 prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c From 661892af7c5c57479a230b77a86226e6fa843120 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Wed, 2 Jul 2025 05:47:49 +0300 Subject: [PATCH 067/181] up mineflayer --- pnpm-lock.yaml | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c904c3f3..bf718ae0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -136,13 +136,13 @@ importers: version: 4.17.21 mcraft-fun-mineflayer: specifier: ^0.1.23 - version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/67db1808c38fab66d77ccf7822ff7c4f59308470(encoding@0.1.13)) + version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/63e351460276245c15ff706fde34e228025dd033(encoding@0.1.13)) minecraft-data: specifier: 3.92.0 version: 3.92.0 minecraft-protocol: specifier: github:PrismarineJS/node-minecraft-protocol#master - version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/1e38d8fc1e1bbe7aa834055cfd38ed0fa22c2085(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) + version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) mineflayer-item-map-downloader: specifier: github:zardoy/mineflayer-item-map-downloader version: https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad)(encoding@0.1.13) @@ -341,7 +341,7 @@ importers: version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/5494f356b1e59eddc876c3ae05ff395f12a46379(@types/react@18.3.18)(react@18.3.1) mineflayer: specifier: github:zardoy/mineflayer#gen-the-master - version: https://codeload.github.com/zardoy/mineflayer/tar.gz/67db1808c38fab66d77ccf7822ff7c4f59308470(encoding@0.1.13) + version: https://codeload.github.com/zardoy/mineflayer/tar.gz/63e351460276245c15ff706fde34e228025dd033(encoding@0.1.13) mineflayer-mouse: specifier: ^0.1.10 version: 0.1.10(@types/debug@4.1.12)(@types/node@22.13.9)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) @@ -6709,9 +6709,9 @@ packages: resolution: {tarball: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/5494f356b1e59eddc876c3ae05ff395f12a46379} version: 1.0.1 - minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/1e38d8fc1e1bbe7aa834055cfd38ed0fa22c2085: - resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/1e38d8fc1e1bbe7aa834055cfd38ed0fa22c2085} - version: 1.57.0 + minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176: + resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176} + version: 1.58.0 engines: {node: '>=22'} minecraft-wrap@1.6.0: @@ -6732,13 +6732,13 @@ packages: mineflayer-pathfinder@2.4.5: resolution: {integrity: sha512-Jh3JnUgRLwhMh2Dugo4SPza68C41y+NPP5sdsgxRu35ydndo70i1JJGxauVWbXrpNwIxYNztUw78aFyb7icw8g==} - mineflayer@4.27.0: - resolution: {integrity: sha512-3bxph4jfbkBh5HpeouorxzrfSLNV+i+1gugNJ2jf52HW+rt+tW7eiiFPxrJEsOVkPT/3O/dEIW7j93LRlojMkQ==} + mineflayer@4.30.0: + resolution: {integrity: sha512-GtW4hkijyZbSu5LKYYD89xZu+XY7OoP7IkrCnNEn6EdPm0+vr2THoJgFGKrlze9/81+T+P3E4qvJXNFiU/zeJg==} engines: {node: '>=22'} - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/67db1808c38fab66d77ccf7822ff7c4f59308470: - resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/67db1808c38fab66d77ccf7822ff7c4f59308470} - version: 4.27.0 + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/63e351460276245c15ff706fde34e228025dd033: + resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/63e351460276245c15ff706fde34e228025dd033} + version: 4.30.0 engines: {node: '>=22'} minimalistic-assert@1.0.1: @@ -11423,7 +11423,7 @@ snapshots: dependencies: '@nxg-org/mineflayer-util-plugin': 1.8.4 minecraft-data: 3.92.0 - mineflayer: 4.27.0(encoding@0.1.13) + mineflayer: 4.30.0(encoding@0.1.13) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-item: 1.16.0 prismarine-physics: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b @@ -13224,7 +13224,7 @@ snapshots: flatmap: 0.0.3 long: 5.3.1 minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/1e38d8fc1e1bbe7aa834055cfd38ed0fa22c2085(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 @@ -13260,7 +13260,7 @@ snapshots: flatmap: 0.0.3 long: 5.3.1 minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/1e38d8fc1e1bbe7aa834055cfd38ed0fa22c2085(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 @@ -17127,12 +17127,12 @@ snapshots: maxrects-packer: '@zardoy/maxrects-packer@2.7.4' zod: 3.24.2 - mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/67db1808c38fab66d77ccf7822ff7c4f59308470(encoding@0.1.13)): + mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/63e351460276245c15ff706fde34e228025dd033(encoding@0.1.13)): dependencies: '@zardoy/flying-squid': 0.0.49(encoding@0.1.13) exit-hook: 2.2.1 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/1e38d8fc1e1bbe7aa834055cfd38ed0fa22c2085(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) - mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/67db1808c38fab66d77ccf7822ff7c4f59308470(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) + mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/63e351460276245c15ff706fde34e228025dd033(encoding@0.1.13) prismarine-item: 1.16.0 ws: 8.18.1 transitivePeerDependencies: @@ -17450,7 +17450,7 @@ snapshots: - '@types/react' - react - minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/1e38d8fc1e1bbe7aa834055cfd38ed0fa22c2085(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13): + minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13): dependencies: '@types/node-rsa': 1.1.4 '@types/readable-stream': 4.0.18 @@ -17502,7 +17502,7 @@ snapshots: mineflayer-item-map-downloader@https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad)(encoding@0.1.13): dependencies: - mineflayer: 4.27.0(encoding@0.1.13) + mineflayer: 4.30.0(encoding@0.1.13) sharp: 0.30.7 transitivePeerDependencies: - encoding @@ -17546,10 +17546,10 @@ snapshots: prismarine-physics: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b vec3: 0.1.10 - mineflayer@4.27.0(encoding@0.1.13): + mineflayer@4.30.0(encoding@0.1.13): dependencies: minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/1e38d8fc1e1bbe7aa834055cfd38ed0fa22c2085(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chat: 1.11.0 @@ -17569,11 +17569,11 @@ snapshots: - encoding - supports-color - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/67db1808c38fab66d77ccf7822ff7c4f59308470(encoding@0.1.13): + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/63e351460276245c15ff706fde34e228025dd033(encoding@0.1.13): dependencies: '@nxg-org/mineflayer-physics-util': 1.8.10 minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/1e38d8fc1e1bbe7aa834055cfd38ed0fa22c2085(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chat: 1.11.0 From b1298cbe1f4c1c8fddf6a6d9df38edb8354915bb Mon Sep 17 00:00:00 2001 From: Max Lee <max@themoep.de> Date: Wed, 2 Jul 2025 02:50:15 +0000 Subject: [PATCH 068/181] feat: Config option to proxy skin textures (#382) --- config.json | 1 + renderer/viewer/lib/worldrendererCommon.ts | 1 + renderer/viewer/three/entities.ts | 5 +++++ renderer/viewer/three/worldrendererThree.ts | 7 ++++++- src/appConfig.ts | 3 +++ src/entities.ts | 13 +++++++++++-- 6 files changed, 27 insertions(+), 3 deletions(-) diff --git a/config.json b/config.json index fe26a9e5..eed32396 100644 --- a/config.json +++ b/config.json @@ -3,6 +3,7 @@ "defaultHost": "<from-proxy>", "defaultProxy": "https://proxy.mcraft.fun", "mapsProvider": "https://maps.mcraft.fun/", + "skinTexturesProxy": "", "peerJsServer": "", "peerJsServerFallback": "https://p2p.mcraft.fun", "promoteServers": [ diff --git a/renderer/viewer/lib/worldrendererCommon.ts b/renderer/viewer/lib/worldrendererCommon.ts index 4441201c..7501b279 100644 --- a/renderer/viewer/lib/worldrendererCommon.ts +++ b/renderer/viewer/lib/worldrendererCommon.ts @@ -36,6 +36,7 @@ export const defaultWorldRendererConfig = { mesherWorkers: 4, isPlayground: false, renderEars: true, + skinTexturesProxy: undefined as string | undefined, // game renderer setting actually showHand: false, viewBobbing: false, diff --git a/renderer/viewer/three/entities.ts b/renderer/viewer/three/entities.ts index 3e37102c..fd67ea80 100644 --- a/renderer/viewer/three/entities.ts +++ b/renderer/viewer/three/entities.ts @@ -1404,6 +1404,11 @@ function addArmorModel (worldRenderer: WorldRendererThree, entityMesh: THREE.Obj if (textureData) { const decodedData = JSON.parse(Buffer.from(textureData, 'base64').toString()) texturePath = decodedData.textures?.SKIN?.url + const { skinTexturesProxy } = this.worldRenderer.worldRendererConfig + if (skinTexturesProxy) { + texturePath = texturePath?.replace('http://textures.minecraft.net/', skinTexturesProxy) + .replace('https://textures.minecraft.net/', skinTexturesProxy) + } } } catch (err) { console.error('Error decoding player head texture:', err) diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index c4482052..e5b93f88 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -755,7 +755,12 @@ export class WorldRendererThree extends WorldRendererCommon { try { const textureData = JSON.parse(Buffer.from(textures.Value, 'base64').toString()) - const skinUrl = textureData.textures?.SKIN?.url + let skinUrl = textureData.textures?.SKIN?.url + const { skinTexturesProxy } = this.worldRendererConfig + if (skinTexturesProxy) { + skinUrl = skinUrl?.replace('http://textures.minecraft.net/', skinTexturesProxy) + .replace('https://textures.minecraft.net/', skinTexturesProxy) + } const mesh = getMesh(this, skinUrl, armorModel.head) const group = new THREE.Group() diff --git a/src/appConfig.ts b/src/appConfig.ts index 261ec7d3..ca1d513c 100644 --- a/src/appConfig.ts +++ b/src/appConfig.ts @@ -54,6 +54,7 @@ export type AppConfig = { supportedLanguages?: string[] showModsButton?: boolean defaultUsername?: string + skinTexturesProxy?: string } export const loadAppConfig = (appConfig: AppConfig) => { @@ -82,6 +83,8 @@ export const loadAppConfig = (appConfig: AppConfig) => { updateBinds(customKeymaps) } + appViewer.inWorldRenderingConfig.skinTexturesProxy = appConfig.skinTexturesProxy + setStorageDataOnAppConfigLoad(appConfig) } diff --git a/src/entities.ts b/src/entities.ts index b9f7998e..2ff51a48 100644 --- a/src/entities.ts +++ b/src/entities.ts @@ -199,6 +199,15 @@ customEvents.on('gameLoaded', () => { // Texture override from packet properties bot._client.on('player_info', (packet) => { + const applySkinTexturesProxy = (url: string) => { + const { appConfig } = miscUiState + if (appConfig?.skinTexturesProxy) { + return url?.replace('http://textures.minecraft.net/', appConfig.skinTexturesProxy) + .replace('https://textures.minecraft.net/', appConfig.skinTexturesProxy) + } + return url + } + for (const playerEntry of packet.data) { if (!playerEntry.player && !playerEntry.properties) continue let textureProperty = playerEntry.properties?.find(prop => prop?.name === 'textures') @@ -208,8 +217,8 @@ customEvents.on('gameLoaded', () => { if (textureProperty) { try { const textureData = JSON.parse(Buffer.from(textureProperty.value, 'base64').toString()) - const skinUrl = textureData.textures?.SKIN?.url - const capeUrl = textureData.textures?.CAPE?.url + const skinUrl = applySkinTexturesProxy(textureData.textures?.SKIN?.url) + const capeUrl = applySkinTexturesProxy(textureData.textures?.CAPE?.url) // Find entity with matching UUID and update skin let entityId = '' From 3bcf0f533a062ade905a4d5e247a24a4d37d59d0 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Wed, 2 Jul 2025 06:06:21 +0300 Subject: [PATCH 069/181] up protocol patch --- patches/minecraft-protocol.patch | 50 ++++++++++++++++---------------- pnpm-lock.yaml | 17 +++++------ 2 files changed, 32 insertions(+), 35 deletions(-) diff --git a/patches/minecraft-protocol.patch b/patches/minecraft-protocol.patch index a3a4a6a3..6a82d5cf 100644 --- a/patches/minecraft-protocol.patch +++ b/patches/minecraft-protocol.patch @@ -1,26 +1,26 @@ diff --git a/src/client/chat.js b/src/client/chat.js -index f14269bea055d4329cd729271e7406ec4b344de7..00f5482eb6e3c911381ca9a728b1b4aae0d1d337 100644 +index 8d0869b150681574ad19292a026cce9f67a137ee..2efa2e6600f017b566155974cb9fb1856fa582f9 100644 --- a/src/client/chat.js +++ b/src/client/chat.js -@@ -111,7 +111,7 @@ module.exports = function (client, options) { - for (const player of packet.data) { - if (!player.chatSession) continue - client._players[player.UUID] = { -- publicKey: crypto.createPublicKey({ key: player.chatSession.publicKey.keyBytes, format: 'der', type: 'spki' }), -+ // publicKey: crypto.createPublicKey({ key: player.chatSession.publicKey.keyBytes, format: 'der', type: 'spki' }), - publicKeyDER: player.chatSession.publicKey.keyBytes, - sessionUuid: player.chatSession.uuid - } -@@ -127,7 +127,7 @@ module.exports = function (client, options) { - for (const player of packet.data) { - if (player.crypto) { - client._players[player.UUID] = { -- publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }), -+ // publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }), - publicKeyDER: player.crypto.publicKey, - signature: player.crypto.signature, - displayName: player.displayName || player.name -@@ -198,7 +198,7 @@ module.exports = function (client, options) { +@@ -109,7 +109,7 @@ module.exports = function (client, options) { + for (const player of packet.data) { + if (player.chatSession) { + client._players[player.uuid] = { +- publicKey: crypto.createPublicKey({ key: player.chatSession.publicKey.keyBytes, format: 'der', type: 'spki' }), ++ // publicKey: crypto.createPublicKey({ key: player.chatSession.publicKey.keyBytes, format: 'der', type: 'spki' }), + publicKeyDER: player.chatSession.publicKey.keyBytes, + sessionUuid: player.chatSession.uuid + } +@@ -119,7 +119,7 @@ module.exports = function (client, options) { + + if (player.crypto) { + client._players[player.uuid] = { +- publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }), ++ // publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }), + publicKeyDER: player.crypto.publicKey, + signature: player.crypto.signature, + displayName: player.displayName || player.name +@@ -189,7 +189,7 @@ module.exports = function (client, options) { if (mcData.supportFeature('useChatSessions')) { const tsDelta = BigInt(Date.now()) - packet.timestamp const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0 @@ -29,7 +29,7 @@ index f14269bea055d4329cd729271e7406ec4b344de7..00f5482eb6e3c911381ca9a728b1b4aa if (verified) client._signatureCache.push(packet.signature) client.emit('playerChat', { plainMessage: packet.plainMessage, -@@ -363,7 +363,7 @@ module.exports = function (client, options) { +@@ -354,7 +354,7 @@ module.exports = function (client, options) { } } @@ -38,7 +38,7 @@ index f14269bea055d4329cd729271e7406ec4b344de7..00f5482eb6e3c911381ca9a728b1b4aa options.timestamp = options.timestamp || BigInt(Date.now()) options.salt = options.salt || 1n -@@ -405,7 +405,7 @@ module.exports = function (client, options) { +@@ -396,7 +396,7 @@ module.exports = function (client, options) { message, timestamp: options.timestamp, salt: options.salt, @@ -47,7 +47,7 @@ index f14269bea055d4329cd729271e7406ec4b344de7..00f5482eb6e3c911381ca9a728b1b4aa offset: client._lastSeenMessages.pending, acknowledged }) -@@ -419,7 +419,7 @@ module.exports = function (client, options) { +@@ -410,7 +410,7 @@ module.exports = function (client, options) { message, timestamp: options.timestamp, salt: options.salt, @@ -74,7 +74,7 @@ index b9d21bab9faccd5dbf1975fc423fc55c73e906c5..99ffd76527b410e3a393181beb260108 function onJoinServerResponse (err) { diff --git a/src/client/play.js b/src/client/play.js -index 6e06dc15291b38e1eeeec8d7102187b2a23d70a3..f67454942db9276cbb9eab99c281cfe182cb8a1f 100644 +index 559607f34e9a5b2b7809423f8ca4cd6746b60225..4dc1c3139438cc2729b05c57e57bd00252728f8a 100644 --- a/src/client/play.js +++ b/src/client/play.js @@ -53,7 +53,7 @@ module.exports = function (client, options) { @@ -87,7 +87,7 @@ index 6e06dc15291b38e1eeeec8d7102187b2a23d70a3..f67454942db9276cbb9eab99c281cfe1 }) // Server should send finish_configuration on its own right after sending the client a dimension codec diff --git a/src/client.js b/src/client.js -index 74749698f8cee05b5dc749c271544f78d06645b0..e77e0a3f41c1ee780c3abbd54b0801d248c2a07c 100644 +index 5c7a62b013daa69be91ec9e763b1f48ffe96ffa6..174d42a77740a937afcb106e1f39a9ee824a24b9 100644 --- a/src/client.js +++ b/src/client.js @@ -89,10 +89,12 @@ class Client extends EventEmitter { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf718ae0..2ad22c38 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,9 +21,6 @@ overrides: prismarine-item: latest patchedDependencies: - minecraft-protocol: - hash: 09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762 - path: patches/minecraft-protocol.patch mineflayer-item-map-downloader@1.2.0: hash: a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad path: patches/mineflayer-item-map-downloader@1.2.0.patch @@ -142,7 +139,7 @@ importers: version: 3.92.0 minecraft-protocol: specifier: github:PrismarineJS/node-minecraft-protocol#master - version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) + version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(encoding@0.1.13) mineflayer-item-map-downloader: specifier: github:zardoy/mineflayer-item-map-downloader version: https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad)(encoding@0.1.13) @@ -13224,7 +13221,7 @@ snapshots: flatmap: 0.0.3 long: 5.3.1 minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 @@ -13260,7 +13257,7 @@ snapshots: flatmap: 0.0.3 long: 5.3.1 minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 @@ -17131,7 +17128,7 @@ snapshots: dependencies: '@zardoy/flying-squid': 0.0.49(encoding@0.1.13) exit-hook: 2.2.1 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(encoding@0.1.13) mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/63e351460276245c15ff706fde34e228025dd033(encoding@0.1.13) prismarine-item: 1.16.0 ws: 8.18.1 @@ -17450,7 +17447,7 @@ snapshots: - '@types/react' - react - minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13): + minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(encoding@0.1.13): dependencies: '@types/node-rsa': 1.1.4 '@types/readable-stream': 4.0.18 @@ -17549,7 +17546,7 @@ snapshots: mineflayer@4.30.0(encoding@0.1.13): dependencies: minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(encoding@0.1.13) prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chat: 1.11.0 @@ -17573,7 +17570,7 @@ snapshots: dependencies: '@nxg-org/mineflayer-physics-util': 1.8.10 minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=09def7a73311f10b6aa8ff9f9b76129589578fa154a8b846b06909ca748c4762)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(encoding@0.1.13) prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chat: 1.11.0 From 7aea07f83a2f8fbbe0379e74611e0c09565c2bc4 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Wed, 2 Jul 2025 06:13:40 +0300 Subject: [PATCH 070/181] up patch again --- pnpm-lock.yaml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ad22c38..90f9b15f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,9 @@ overrides: prismarine-item: latest patchedDependencies: + minecraft-protocol: + hash: a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0 + path: patches/minecraft-protocol.patch mineflayer-item-map-downloader@1.2.0: hash: a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad path: patches/mineflayer-item-map-downloader@1.2.0.patch @@ -139,7 +142,7 @@ importers: version: 3.92.0 minecraft-protocol: specifier: github:PrismarineJS/node-minecraft-protocol#master - version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(encoding@0.1.13) + version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0)(encoding@0.1.13) mineflayer-item-map-downloader: specifier: github:zardoy/mineflayer-item-map-downloader version: https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad)(encoding@0.1.13) @@ -13221,7 +13224,7 @@ snapshots: flatmap: 0.0.3 long: 5.3.1 minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0)(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 @@ -13257,7 +13260,7 @@ snapshots: flatmap: 0.0.3 long: 5.3.1 minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0)(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 @@ -17128,7 +17131,7 @@ snapshots: dependencies: '@zardoy/flying-squid': 0.0.49(encoding@0.1.13) exit-hook: 2.2.1 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0)(encoding@0.1.13) mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/63e351460276245c15ff706fde34e228025dd033(encoding@0.1.13) prismarine-item: 1.16.0 ws: 8.18.1 @@ -17447,7 +17450,7 @@ snapshots: - '@types/react' - react - minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(encoding@0.1.13): + minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0)(encoding@0.1.13): dependencies: '@types/node-rsa': 1.1.4 '@types/readable-stream': 4.0.18 @@ -17546,7 +17549,7 @@ snapshots: mineflayer@4.30.0(encoding@0.1.13): dependencies: minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0)(encoding@0.1.13) prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chat: 1.11.0 @@ -17570,7 +17573,7 @@ snapshots: dependencies: '@nxg-org/mineflayer-physics-util': 1.8.10 minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0)(encoding@0.1.13) prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chat: 1.11.0 From 71257bdf137dc918f7cc762dad1fc600788a1113 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Wed, 2 Jul 2025 06:16:06 +0300 Subject: [PATCH 071/181] add fuchsmc.net server partner back! --- config.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config.json b/config.json index eed32396..57b1c207 100644 --- a/config.json +++ b/config.json @@ -10,6 +10,9 @@ { "ip": "wss://play.mcraft.fun" }, + { + "ip": "wss://ws.fuchsmc.net" + }, { "ip": "wss://play2.mcraft.fun" }, From 8a504123951876a9b1c5281d86ff815fa82e2cba Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Wed, 2 Jul 2025 06:19:31 +0300 Subject: [PATCH 072/181] fix app crash --- src/appConfig.ts | 2 +- src/appViewer.ts | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/appConfig.ts b/src/appConfig.ts index ca1d513c..0805c448 100644 --- a/src/appConfig.ts +++ b/src/appConfig.ts @@ -83,7 +83,7 @@ export const loadAppConfig = (appConfig: AppConfig) => { updateBinds(customKeymaps) } - appViewer.inWorldRenderingConfig.skinTexturesProxy = appConfig.skinTexturesProxy + appViewer?.appConfigUdpate() setStorageDataOnAppConfigLoad(appConfig) } diff --git a/src/appViewer.ts b/src/appViewer.ts index 0be6c2e8..05b1cf41 100644 --- a/src/appViewer.ts +++ b/src/appViewer.ts @@ -180,6 +180,12 @@ export class AppViewer { this.worldView!.listenToBot(bot) } + appConfigUdpate () { + if (miscUiState.appConfig) { + this.inWorldRenderingConfig.skinTexturesProxy = miscUiState.appConfig.skinTexturesProxy + } + } + async startWorld (world, renderDistance: number, playerStateSend: PlayerStateRenderer = this.playerState.reactive) { if (this.currentDisplay === 'world') throw new Error('World already started') this.currentDisplay = 'world' @@ -187,6 +193,7 @@ export class AppViewer { this.worldView = new WorldDataEmitter(world, renderDistance, startPosition) window.worldView = this.worldView watchOptionsAfterWorldViewInit(this.worldView) + this.appConfigUdpate() const displayWorldOptions: DisplayWorldOptions = { version: this.resourcesManager.currentConfig!.version, From 9086435aee77b4c42c9b45254b2bb993b1c92e7e Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Wed, 2 Jul 2025 06:21:15 +0300 Subject: [PATCH 073/181] rm building storybook --- .github/workflows/next-deploy.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/next-deploy.yml b/.github/workflows/next-deploy.yml index 943727eb..99172bf7 100644 --- a/.github/workflows/next-deploy.yml +++ b/.github/workflows/next-deploy.yml @@ -36,7 +36,6 @@ jobs: run: vercel build --token=${{ secrets.VERCEL_TOKEN }} env: CONFIG_JSON_SOURCE: BUNDLED - - run: pnpm build-storybook - name: Copy playground files run: | mkdir -p .vercel/output/static/playground From ed7c33ff9f1a16442f1e89aac92b12214e56f7cc Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Wed, 2 Jul 2025 06:22:17 +0300 Subject: [PATCH 074/181] up mouse --- package.json | 2 +- pnpm-lock.yaml | 270 ++----------------------------------------------- 2 files changed, 7 insertions(+), 265 deletions(-) diff --git a/package.json b/package.json index eb53a8bf..268afaab 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,7 @@ "mc-assets": "^0.2.62", "minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next", "mineflayer": "github:zardoy/mineflayer#gen-the-master", - "mineflayer-mouse": "^0.1.10", + "mineflayer-mouse": "^0.1.11", "mineflayer-pathfinder": "^2.4.4", "npm-run-all": "^4.1.5", "os-browserify": "^0.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 90f9b15f..5bb171b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -343,8 +343,8 @@ importers: specifier: github:zardoy/mineflayer#gen-the-master version: https://codeload.github.com/zardoy/mineflayer/tar.gz/63e351460276245c15ff706fde34e228025dd033(encoding@0.1.13) mineflayer-mouse: - specifier: ^0.1.10 - version: 0.1.10(@types/debug@4.1.12)(@types/node@22.13.9)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) + specifier: ^0.1.11 + version: 0.1.11 mineflayer-pathfinder: specifier: ^2.4.4 version: 2.4.5 @@ -3308,47 +3308,18 @@ packages: '@vitest/expect@0.34.6': resolution: {integrity: sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==} - '@vitest/expect@3.0.8': - resolution: {integrity: sha512-Xu6TTIavTvSSS6LZaA3EebWFr6tsoXPetOWNMOlc7LO88QVVBwq2oQWBoDiLCN6YTvNYsGSjqOO8CAdjom5DCQ==} - - '@vitest/mocker@3.0.8': - resolution: {integrity: sha512-n3LjS7fcW1BCoF+zWZxG7/5XvuYH+lsFg+BDwwAz0arIwHQJFUEsKBQ0BLU49fCxuM/2HSeBPHQD8WjgrxMfow==} - peerDependencies: - msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - - '@vitest/pretty-format@3.0.8': - resolution: {integrity: sha512-BNqwbEyitFhzYMYHUVbIvepOyeQOSFA/NeJMIP9enMntkkxLgOcgABH6fjyXG85ipTgvero6noreavGIqfJcIg==} - '@vitest/runner@0.34.6': resolution: {integrity: sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==} - '@vitest/runner@3.0.8': - resolution: {integrity: sha512-c7UUw6gEcOzI8fih+uaAXS5DwjlBaCJUo7KJ4VvJcjL95+DSR1kova2hFuRt3w41KZEFcOEiq098KkyrjXeM5w==} - '@vitest/snapshot@0.34.6': resolution: {integrity: sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==} - '@vitest/snapshot@3.0.8': - resolution: {integrity: sha512-x8IlMGSEMugakInj44nUrLSILh/zy1f2/BgH0UeHpNyOocG18M9CWVIFBaXPt8TrqVZWmcPjwfG/ht5tnpba8A==} - '@vitest/spy@0.34.6': resolution: {integrity: sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==} - '@vitest/spy@3.0.8': - resolution: {integrity: sha512-MR+PzJa+22vFKYb934CejhR4BeRpMSoxkvNoDit68GQxRLSf11aT6CTj3XaqUU9rxgWJFnqicN/wxw6yBRkI1Q==} - '@vitest/utils@0.34.6': resolution: {integrity: sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==} - '@vitest/utils@3.0.8': - resolution: {integrity: sha512-nkBC3aEhfX2PdtQI/QwAWp8qZWwzASsU4Npbcd5RdMPBSSLCpkZp52P3xku3s3uA0HIEhGvEcF8rNkBsz9dQ4Q==} - '@xboxreplay/errors@0.1.0': resolution: {integrity: sha512-Tgz1d/OIPDWPeyOvuL5+aai5VCcqObhPnlI3skQuf80GVF3k1I0lPCnGC+8Cm5PV9aLBT5m8qPcJoIUQ2U4y9g==} @@ -3672,10 +3643,6 @@ packages: assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} - assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} - assign-symbols@1.0.0: resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==} engines: {node: '>=0.10.0'} @@ -4012,10 +3979,6 @@ packages: resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} engines: {node: '>=4'} - chai@5.2.0: - resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} - engines: {node: '>=12'} - chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -4043,10 +4006,6 @@ packages: check-error@1.0.3: resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} - check-error@2.1.1: - resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} - engines: {node: '>= 16'} - check-more-types@2.24.0: resolution: {integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==} engines: {node: '>= 0.8.0'} @@ -4472,10 +4431,6 @@ packages: resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} engines: {node: '>=6'} - deep-eql@5.0.2: - resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} - engines: {node: '>=6'} - deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -4796,9 +4751,6 @@ packages: es-module-lexer@0.9.3: resolution: {integrity: sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==} - es-module-lexer@1.6.0: - resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} - es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -5045,9 +4997,6 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -5103,10 +5052,6 @@ packages: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} - expect-type@1.2.0: - resolution: {integrity: sha512-80F22aiJ3GLyVnS/B3HzgR6RelZVumzj9jkL0Rhz4h0xYbNW9PjlQz5h3J/SShErbXBc295vseR4/MIbVmUbeA==} - engines: {node: '>=12.0.0'} - exponential-backoff@3.1.2: resolution: {integrity: sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==} @@ -6403,9 +6348,6 @@ packages: loupe@2.3.7: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} - loupe@3.1.3: - resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} - lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -6725,8 +6667,8 @@ packages: resolution: {tarball: https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824} version: 1.2.0 - mineflayer-mouse@0.1.10: - resolution: {integrity: sha512-bxrBzOVQX2Ok5KOiJMaoOLYaHMDecMkJpLn5oGIdyLRqiTF3WAnZc9oYQq+/f3YNA88W1vBIjai3v3vZe6wyaQ==} + mineflayer-mouse@0.1.11: + resolution: {integrity: sha512-BL47pXZ1+92BA/7ym6KaJctEHKnL0up+tpuagVwSKJvAgibeqWQJJwDlNUWkOLvpnruRKDxMR5OB1hUXFoDNSg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} mineflayer-pathfinder@2.4.5: @@ -7271,10 +7213,6 @@ packages: pathval@1.1.1: resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} - pathval@2.0.0: - resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} - engines: {node: '>= 14.16'} - pause-stream@0.0.11: resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} @@ -8716,22 +8654,10 @@ packages: resolution: {integrity: sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==} engines: {node: '>=14.0.0'} - tinypool@1.0.2: - resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} - engines: {node: ^18.0.0 || >=20.0.0} - - tinyrainbow@2.0.0: - resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} - engines: {node: '>=14.0.0'} - tinyspy@2.2.1: resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} engines: {node: '>=14.0.0'} - tinyspy@3.0.2: - resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} - engines: {node: '>=14.0.0'} - title-case@3.0.3: resolution: {integrity: sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==} @@ -9218,11 +9144,6 @@ packages: engines: {node: '>=v14.18.0'} hasBin: true - vite-node@3.0.8: - resolution: {integrity: sha512-6PhR4H9VGlcwXZ+KWCdMqbtG649xCPZqfI9j2PsK1FcXgEzro5bGHcVKFCTqPLaNKZES8Evqv4LwvZARsq5qlg==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - vite@4.5.9: resolution: {integrity: sha512-qK9W4xjgD3gXbC0NmdNFFnVFLMWSNiR3swj957yutwzzN16xF/E7nmtAyp1rT9hviDroQANjE4HK3H4WqWdFtw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -9322,34 +9243,6 @@ packages: webdriverio: optional: true - vitest@3.0.8: - resolution: {integrity: sha512-dfqAsNqRGUc8hB9OVR2P0w8PZPEckti2+5rdZip0WIz9WW0MnImJ8XiR61QhqLa92EQzKP2uPkzenKOAHyEIbA==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/debug': ^4.1.12 - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.0.8 - '@vitest/ui': 3.0.8 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/debug': - optional: true - '@types/node': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - vm-browserify@1.1.2: resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} @@ -13086,68 +12979,28 @@ snapshots: '@vitest/utils': 0.34.6 chai: 4.5.0 - '@vitest/expect@3.0.8': - dependencies: - '@vitest/spy': 3.0.8 - '@vitest/utils': 3.0.8 - chai: 5.2.0 - tinyrainbow: 2.0.0 - - '@vitest/mocker@3.0.8(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))': - dependencies: - '@vitest/spy': 3.0.8 - estree-walker: 3.0.3 - magic-string: 0.30.17 - optionalDependencies: - vite: 6.2.1(@types/node@22.13.9)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) - - '@vitest/pretty-format@3.0.8': - dependencies: - tinyrainbow: 2.0.0 - '@vitest/runner@0.34.6': dependencies: '@vitest/utils': 0.34.6 p-limit: 4.0.0 pathe: 1.1.2 - '@vitest/runner@3.0.8': - dependencies: - '@vitest/utils': 3.0.8 - pathe: 2.0.3 - '@vitest/snapshot@0.34.6': dependencies: magic-string: 0.30.17 pathe: 1.1.2 pretty-format: 29.7.0 - '@vitest/snapshot@3.0.8': - dependencies: - '@vitest/pretty-format': 3.0.8 - magic-string: 0.30.17 - pathe: 2.0.3 - '@vitest/spy@0.34.6': dependencies: tinyspy: 2.2.1 - '@vitest/spy@3.0.8': - dependencies: - tinyspy: 3.0.2 - '@vitest/utils@0.34.6': dependencies: diff-sequences: 29.6.3 loupe: 2.3.7 pretty-format: 29.7.0 - '@vitest/utils@3.0.8': - dependencies: - '@vitest/pretty-format': 3.0.8 - loupe: 3.1.3 - tinyrainbow: 2.0.0 - '@xboxreplay/errors@0.1.0': {} '@xboxreplay/xboxlive-auth@3.3.3(debug@4.4.0)': @@ -13562,8 +13415,6 @@ snapshots: assertion-error@1.1.0: {} - assertion-error@2.0.1: {} - assign-symbols@1.0.0: {} ast-types@0.16.1: @@ -13995,14 +13846,6 @@ snapshots: pathval: 1.1.1 type-detect: 4.1.0 - chai@5.2.0: - dependencies: - assertion-error: 2.0.1 - check-error: 2.1.1 - deep-eql: 5.0.2 - loupe: 3.1.3 - pathval: 2.0.0 - chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -14041,8 +13884,6 @@ snapshots: dependencies: get-func-name: 2.0.2 - check-error@2.1.1: {} - check-more-types@2.24.0: optional: true @@ -14557,8 +14398,6 @@ snapshots: dependencies: type-detect: 4.1.0 - deep-eql@5.0.2: {} - deep-extend@0.6.0: {} deep-is@0.1.4: {} @@ -15056,8 +14895,6 @@ snapshots: es-module-lexer@0.9.3: {} - es-module-lexer@1.6.0: {} - es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -15440,10 +15277,6 @@ snapshots: estree-walker@2.0.2: {} - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.6 - esutils@2.0.3: {} etag@1.8.1: {} @@ -15521,8 +15354,6 @@ snapshots: expand-template@2.0.3: {} - expect-type@1.2.0: {} - exponential-backoff@3.1.2: optional: true @@ -17024,8 +16855,6 @@ snapshots: dependencies: get-func-name: 2.0.2 - loupe@3.1.3: {} - lower-case@2.0.2: dependencies: tslib: 2.8.1 @@ -17508,33 +17337,14 @@ snapshots: - encoding - supports-color - mineflayer-mouse@0.1.10(@types/debug@4.1.12)(@types/node@22.13.9)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0): + mineflayer-mouse@0.1.11: dependencies: change-case: 5.4.4 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 prismarine-item: 1.16.0 prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c - vitest: 3.0.8(@types/debug@4.1.12)(@types/node@22.13.9)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) transitivePeerDependencies: - - '@edge-runtime/vm' - - '@types/debug' - - '@types/node' - - '@vitest/browser' - - '@vitest/ui' - - happy-dom - - jiti - - jsdom - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - supports-color - - terser - - tsx - - yaml mineflayer-pathfinder@2.4.5: dependencies: @@ -18202,8 +18012,6 @@ snapshots: pathval@1.1.1: {} - pathval@2.0.0: {} - pause-stream@0.0.11: dependencies: through: 2.3.8 @@ -20064,14 +19872,8 @@ snapshots: tinypool@0.7.0: {} - tinypool@1.0.2: {} - - tinyrainbow@2.0.0: {} - tinyspy@2.2.1: {} - tinyspy@3.0.2: {} - title-case@3.0.3: dependencies: tslib: 2.8.1 @@ -20560,27 +20362,6 @@ snapshots: - supports-color - terser - vite-node@3.0.8(@types/node@22.13.9)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0): - dependencies: - cac: 6.7.14 - debug: 4.4.1 - es-module-lexer: 1.6.0 - pathe: 2.0.3 - vite: 6.2.1(@types/node@22.13.9)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - vite@4.5.9(@types/node@22.13.9)(terser@5.39.0): dependencies: esbuild: 0.18.20 @@ -20639,45 +20420,6 @@ snapshots: - supports-color - terser - vitest@3.0.8(@types/debug@4.1.12)(@types/node@22.13.9)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0): - dependencies: - '@vitest/expect': 3.0.8 - '@vitest/mocker': 3.0.8(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)) - '@vitest/pretty-format': 3.0.8 - '@vitest/runner': 3.0.8 - '@vitest/snapshot': 3.0.8 - '@vitest/spy': 3.0.8 - '@vitest/utils': 3.0.8 - chai: 5.2.0 - debug: 4.4.1 - expect-type: 1.2.0 - magic-string: 0.30.17 - pathe: 2.0.3 - std-env: 3.8.1 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinypool: 1.0.2 - tinyrainbow: 2.0.0 - vite: 6.2.1(@types/node@22.13.9)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) - vite-node: 3.0.8(@types/node@22.13.9)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/debug': 4.1.12 - '@types/node': 22.13.9 - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - vm-browserify@1.1.2: {} w3c-keyname@2.2.8: {} From 3320f65b9c1a075e07138d762b8114e85136128a Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Wed, 2 Jul 2025 06:24:02 +0300 Subject: [PATCH 075/181] now app crash should be fixed --- README.MD | 4 ++-- src/globals.js | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.MD b/README.MD index ea5a1453..36066fef 100644 --- a/README.MD +++ b/README.MD @@ -127,7 +127,7 @@ Instead I recommend setting `options.debugLogNotFrequentPackets`. Also you can u - `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. +- `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`/`server` - Only for singleplayer mode/host. Flying Squid server instance, see it's documentation for more. @@ -136,7 +136,7 @@ Instead I recommend setting `options.debugLogNotFrequentPackets`. Also you can u - `nbt.simplify(someNbt)` - Simplifies nbt data, so it's easier to read. -The most useful thing in devtools is the watch expression. You can add any expression there and it will be re-evaluated in real time. For example, you can add `viewer.camera.position` to see the camera position and so on. +The most useful thing in devtools is the watch expression. You can add any expression there and it will be re-evaluated in real time. For example, you can add `camera.position` to see the camera position and so on. <img src="./docs-assets/watch-expr.png" alt="Watch expression" width="480"/> diff --git a/src/globals.js b/src/globals.js index f9125c8c..11351555 100644 --- a/src/globals.js +++ b/src/globals.js @@ -5,7 +5,8 @@ window.bot = undefined window.THREE = undefined window.localServer = undefined window.worldView = undefined -window.viewer = undefined +window.viewer = undefined // legacy +window.appViewer = undefined window.loadedData = undefined window.customEvents = new EventEmitter() window.customEvents.setMaxListeners(10_000) From c913d63c46a8f7c05987cb442d60fc0dc48f4eb4 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Wed, 2 Jul 2025 16:25:36 +0300 Subject: [PATCH 076/181] fix(regression): fix ios 16 world rendering support! --- experiments/three.ts | 134 ++++++++++------------------- renderer/viewer/lib/utils/skins.ts | 16 +++- 2 files changed, 62 insertions(+), 88 deletions(-) diff --git a/experiments/three.ts b/experiments/three.ts index 7a629a13..9b158dec 100644 --- a/experiments/three.ts +++ b/experiments/three.ts @@ -1,101 +1,61 @@ import * as THREE from 'three' -import * as tweenJs from '@tweenjs/tween.js' -import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' -import * as THREE from 'three'; -import Jimp from 'jimp'; +import { loadThreeJsTextureFromBitmap } from '../renderer/viewer/lib/utils/skins' +// Create scene, camera and renderer const scene = new THREE.Scene() const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000) -camera.position.set(0, 0, 5) const renderer = new THREE.WebGLRenderer() renderer.setSize(window.innerWidth, window.innerHeight) document.body.appendChild(renderer.domElement) -const controls = new OrbitControls(camera, renderer.domElement) +// Position camera +camera.position.z = 5 -const geometry = new THREE.BoxGeometry(1, 1, 1) -const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }) -const cube = new THREE.Mesh(geometry, material) -cube.position.set(0.5, 0.5, 0.5); -const group = new THREE.Group() -group.add(cube) -group.position.set(-0.5, -0.5, -0.5); -const outerGroup = new THREE.Group() -outerGroup.add(group) -outerGroup.scale.set(0.2, 0.2, 0.2) -outerGroup.position.set(1, 1, 0) -scene.add(outerGroup) +// Create a canvas with some content +const canvas = document.createElement('canvas') +canvas.width = 256 +canvas.height = 256 +const ctx = canvas.getContext('2d') -// const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshBasicMaterial({ color: 0x00_00_ff, transparent: true, opacity: 0.5 })) -// mesh.position.set(0.5, 1, 0.5) -// const group = new THREE.Group() -// group.add(mesh) -// group.position.set(-0.5, -1, -0.5) -// const outerGroup = new THREE.Group() -// outerGroup.add(group) -// // outerGroup.position.set(this.camera.position.x, this.camera.position.y, this.camera.position.z) -// scene.add(outerGroup) +scene.background = new THREE.Color(0x444444) - new tweenJs.Tween(group.rotation).to({ z: THREE.MathUtils.degToRad(90) }, 1000).yoyo(true).repeat(Infinity).start() +// Draw something on the canvas +ctx.fillStyle = '#444444' +// ctx.fillRect(0, 0, 256, 256) +ctx.fillStyle = 'red' +ctx.font = '48px Arial' +ctx.textAlign = 'center' +ctx.textBaseline = 'middle' +ctx.fillText('Hello!', 128, 128) -const tweenGroup = new tweenJs.Group() -function animate () { - tweenGroup.update() - requestAnimationFrame(animate) -// cube.rotation.x += 0.01 -// cube.rotation.y += 0.01 - renderer.render(scene, camera) +// Create bitmap and texture +async function createTexturedBox() { + const canvas2 = new OffscreenCanvas(256, 256) + const ctx2 = canvas2.getContext('2d')! + ctx2.drawImage(canvas, 0, 0) + const texture = new THREE.Texture(canvas2) + texture.magFilter = THREE.NearestFilter + texture.minFilter = THREE.NearestFilter + texture.needsUpdate = true + texture.flipY = false + + // Create box with texture + const geometry = new THREE.BoxGeometry(2, 2, 2) + const material = new THREE.MeshBasicMaterial({ + map: texture, + side: THREE.DoubleSide, + premultipliedAlpha: false, + }) + const cube = new THREE.Mesh(geometry, material) + scene.add(cube) +} + +// Create the textured box +createTexturedBox() + +// Animation loop +function animate() { + requestAnimationFrame(animate) + renderer.render(scene, camera) } animate() - -// let animation - -window.animate = () => { - // new Tween.Tween(group.position).to({ y: group.position.y - 1}, 1000 * 0.35/2).yoyo(true).repeat(1).start() - new tweenJs.Tween(group.rotation, tweenGroup).to({ z: THREE.MathUtils.degToRad(90) }, 1000 * 0.35 / 2).yoyo(true).repeat(Infinity).start().onRepeat(() => { - console.log('done') - }) -} - -window.stop = () => { - tweenGroup.removeAll() -} - - -function createGeometryFromImage() { - return new Promise<THREE.ShapeGeometry>((resolve, reject) => { - const img = new Image(); - img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAABEElEQVQ4jWNkIAPw2Zv9J0cfXPOSvx/+L/n74T+HqsJ/JlI1T9u3i6H91B7ybdY+vgZuO1majV+fppFmPnuz/+ihy2dv9t/49Wm8mlECkV1FHh5FfPZm/1XXTGX4cechA4eKPMNVq1CGH7cfMBJ0rlxX+X8OVYX/xq9P/5frKifoZ0Z0AwS8HRkYGBgYvt+8xyDXUUbQZgwJPnuz/+wq8gw/7zxk+PXsFUFno0h6mon+l5fgZFhwnYmBTUqMgYGBgaAhLMiaHQyFGOZvf8Lw49FXRgYGhv8MDAwwg/7jMoQFFury/C8Y5m9/wnADohnZVryJhoWBARJ9Cw69gtmMAgiFAcuvZ68Yfj17hU8NXgAATdKfkzbQhBEAAAAASUVORK5CYII=' - console.log('img.complete', img.complete) - img.onload = () => { - const canvas = document.createElement('canvas'); - canvas.width = img.width; - canvas.height = img.height; - const context = canvas.getContext('2d'); - context.drawImage(img, 0, 0, img.width, img.height); - const imgData = context.getImageData(0, 0, img.width, img.height); - - const shape = new THREE.Shape(); - for (let y = 0; y < img.height; y++) { - for (let x = 0; x < img.width; x++) { - const index = (y * img.width + x) * 4; - const alpha = imgData.data[index + 3]; - if (alpha !== 0) { - shape.lineTo(x, y); - } - } - } - - const geometry = new THREE.ShapeGeometry(shape); - resolve(geometry); - }; - img.onerror = reject; - }); -} - -// Usage: -const shapeGeomtry = createGeometryFromImage().then(geometry => { - const material = new THREE.MeshBasicMaterial({ color: 0xffffff }); - const mesh = new THREE.Mesh(geometry, material); - scene.add(mesh); -}) diff --git a/renderer/viewer/lib/utils/skins.ts b/renderer/viewer/lib/utils/skins.ts index de5a3675..9aa97340 100644 --- a/renderer/viewer/lib/utils/skins.ts +++ b/renderer/viewer/lib/utils/skins.ts @@ -3,6 +3,20 @@ import * as THREE from 'three' import stevePng from 'mc-assets/dist/other-textures/latest/entity/player/wide/steve.png' import { getLoadedImage } from 'mc-assets/dist/utils' +const detectFullOffscreenCanvasSupport = () => { + if (typeof OffscreenCanvas === 'undefined') return false + try { + const canvas = new OffscreenCanvas(1, 1) + // Try to get a WebGL context - this will fail on iOS where only 2D is supported (iOS 16) + const gl = canvas.getContext('webgl2') || canvas.getContext('webgl') + return gl !== null + } catch (e) { + return false + } +} + +const hasFullOffscreenCanvasSupport = detectFullOffscreenCanvasSupport() + export const loadThreeJsTextureFromUrlSync = (imageUrl: string) => { const texture = new THREE.Texture() const promise = getLoadedImage(imageUrl).then(image => { @@ -17,7 +31,7 @@ export const loadThreeJsTextureFromUrlSync = (imageUrl: string) => { } export const createCanvas = (width: number, height: number): OffscreenCanvas => { - if (typeof OffscreenCanvas !== 'undefined') { + if (hasFullOffscreenCanvasSupport) { return new OffscreenCanvas(width, height) } const canvas = document.createElement('canvas') From 3cc862b05d4f008cd79502a1063c687882cf7dd4 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Wed, 2 Jul 2025 16:53:30 +0300 Subject: [PATCH 077/181] up mineflayer --- pnpm-lock.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5bb171b4..02e27584 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -136,7 +136,7 @@ importers: version: 4.17.21 mcraft-fun-mineflayer: specifier: ^0.1.23 - version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/63e351460276245c15ff706fde34e228025dd033(encoding@0.1.13)) + version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/1616261f727398d0b14359262726828d24797fcc(encoding@0.1.13)) minecraft-data: specifier: 3.92.0 version: 3.92.0 @@ -341,7 +341,7 @@ importers: version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/5494f356b1e59eddc876c3ae05ff395f12a46379(@types/react@18.3.18)(react@18.3.1) mineflayer: specifier: github:zardoy/mineflayer#gen-the-master - version: https://codeload.github.com/zardoy/mineflayer/tar.gz/63e351460276245c15ff706fde34e228025dd033(encoding@0.1.13) + version: https://codeload.github.com/zardoy/mineflayer/tar.gz/1616261f727398d0b14359262726828d24797fcc(encoding@0.1.13) mineflayer-mouse: specifier: ^0.1.11 version: 0.1.11 @@ -6678,8 +6678,8 @@ packages: resolution: {integrity: sha512-GtW4hkijyZbSu5LKYYD89xZu+XY7OoP7IkrCnNEn6EdPm0+vr2THoJgFGKrlze9/81+T+P3E4qvJXNFiU/zeJg==} engines: {node: '>=22'} - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/63e351460276245c15ff706fde34e228025dd033: - resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/63e351460276245c15ff706fde34e228025dd033} + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/1616261f727398d0b14359262726828d24797fcc: + resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/1616261f727398d0b14359262726828d24797fcc} version: 4.30.0 engines: {node: '>=22'} @@ -16956,12 +16956,12 @@ snapshots: maxrects-packer: '@zardoy/maxrects-packer@2.7.4' zod: 3.24.2 - mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/63e351460276245c15ff706fde34e228025dd033(encoding@0.1.13)): + mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/1616261f727398d0b14359262726828d24797fcc(encoding@0.1.13)): dependencies: '@zardoy/flying-squid': 0.0.49(encoding@0.1.13) exit-hook: 2.2.1 minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0)(encoding@0.1.13) - mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/63e351460276245c15ff706fde34e228025dd033(encoding@0.1.13) + mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/1616261f727398d0b14359262726828d24797fcc(encoding@0.1.13) prismarine-item: 1.16.0 ws: 8.18.1 transitivePeerDependencies: @@ -17379,7 +17379,7 @@ snapshots: - encoding - supports-color - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/63e351460276245c15ff706fde34e228025dd033(encoding@0.1.13): + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/1616261f727398d0b14359262726828d24797fcc(encoding@0.1.13): dependencies: '@nxg-org/mineflayer-physics-util': 1.8.10 minecraft-data: 3.92.0 From 3bf34a8781a9e5c6665b1ec70215526489e46c02 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Wed, 2 Jul 2025 16:55:32 +0300 Subject: [PATCH 078/181] add browserstack partner --- README.MD | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.MD b/README.MD index 36066fef..3205dfb4 100644 --- a/README.MD +++ b/README.MD @@ -45,9 +45,12 @@ All components that are in [Storybook](https://minimap.mcraft.fun/storybook/) ar ### Browser Notes -These browsers have issues with capturing pointer: +This project is tested with BrowserStack ! [BrowserStack](https://www.browserstack.com/) + +Howerver, these browsers have issues: **Opera Mini**: Disable *mouse gestures* in browsre settings to avoid opening new tab on right click hold + **Vivaldi**: Disable Controls -> *Raw Input* in game settings if experiencing issues ### Versions Support From 08fbc67c31fec68bc56a18ba5c26ef2e82f4c6a6 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Wed, 2 Jul 2025 17:22:44 +0300 Subject: [PATCH 079/181] add update git deps script, fix inventory crashes --- .cursor/rules/vars-usage.mdc | 2 + CONTRIBUTING.md | 7 +- package.json | 8 +- pnpm-lock.yaml | 8 +- scripts/updateGitDeps.ts | 160 +++++++++++++++++++++++++++++++++++ 5 files changed, 178 insertions(+), 7 deletions(-) create mode 100644 scripts/updateGitDeps.ts diff --git a/.cursor/rules/vars-usage.mdc b/.cursor/rules/vars-usage.mdc index 7120e7ae..233e0aba 100644 --- a/.cursor/rules/vars-usage.mdc +++ b/.cursor/rules/vars-usage.mdc @@ -14,3 +14,5 @@ Ask AI - Some other global variables that can be used without window prefixes are listed in src/globals.d.ts Rationale: This ensures a clean separation between the Mineflayer logic (server-side/game logic) and the renderer (client-side/view logic), making the renderer portable and testable, and maintains proper usage of global state. + +For more general project contributing guides see CONTRIBUTING.md on like how to setup the project. Use pnpm tsc if needed to validate result with typechecking the whole project. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 06df61fa..a5a3482d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -177,8 +177,13 @@ New React components, improve UI (including mobile support). ## Updating Dependencies -1. Ensure mineflayer fork is up to date with the latest version of mineflayer original repo +1. Use `pnpm update-git-deps` to check and update git dependencies (like mineflayer fork, prismarine packages etc). The script will: + - Show which git dependencies have updates available + - Ask if you want to update them + - Skip dependencies listed in `pnpm.updateConfig.ignoreDependencies` + 2. Update PrismarineJS dependencies to the latest version: `minecraft-data` (be sure to replace the version twice in the package.json), `mineflayer`, `minecraft-protocol`, `prismarine-block`, `prismarine-chunk`, `prismarine-item`, ... + 3. If `minecraft-protocol` patch fails, do this: 1. Remove the patch from `patchedDependencies` in `package.json` 2. Run `pnpm patch minecraft-protocol`, open patch directory diff --git a/package.json b/package.json index 268afaab..a09b92a3 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "run-playground": "run-p watch-mesher watch-other-workers watch-playground", "run-all": "run-p start run-playground", "build-playground": "rsbuild build --config renderer/rsbuild.config.ts", - "watch-playground": "rsbuild dev --config renderer/rsbuild.config.ts" + "watch-playground": "rsbuild dev --config renderer/rsbuild.config.ts", + "update-git-deps": "tsx scripts/updateGitDeps.ts" }, "keywords": [ "prismarine", @@ -210,7 +211,10 @@ "prismarine-item": "latest" }, "updateConfig": { - "ignoreDependencies": [] + "ignoreDependencies": [ + "browserfs", + "google-drive-browserfs" + ] }, "patchedDependencies": { "pixelarticons@1.8.1": "patches/pixelarticons@1.8.1.patch", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 02e27584..b146bd80 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -338,7 +338,7 @@ importers: version: 0.2.62 minecraft-inventory-gui: specifier: github:zardoy/minecraft-inventory-gui#next - version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/5494f356b1e59eddc876c3ae05ff395f12a46379(@types/react@18.3.18)(react@18.3.1) + version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/89c33d396f3fde4804c71f4be3c203ade1833b41(@types/react@18.3.18)(react@18.3.1) mineflayer: specifier: github:zardoy/mineflayer#gen-the-master version: https://codeload.github.com/zardoy/mineflayer/tar.gz/1616261f727398d0b14359262726828d24797fcc(encoding@0.1.13) @@ -6647,8 +6647,8 @@ packages: minecraft-folder-path@1.2.0: resolution: {integrity: sha512-qaUSbKWoOsH9brn0JQuBhxNAzTDMwrOXorwuRxdJKKKDYvZhtml+6GVCUrY5HRiEsieBEjCUnhVpDuQiKsiFaw==} - minecraft-inventory-gui@https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/5494f356b1e59eddc876c3ae05ff395f12a46379: - resolution: {tarball: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/5494f356b1e59eddc876c3ae05ff395f12a46379} + minecraft-inventory-gui@https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/89c33d396f3fde4804c71f4be3c203ade1833b41: + resolution: {tarball: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/89c33d396f3fde4804c71f4be3c203ade1833b41} version: 1.0.1 minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176: @@ -17272,7 +17272,7 @@ snapshots: minecraft-folder-path@1.2.0: {} - minecraft-inventory-gui@https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/5494f356b1e59eddc876c3ae05ff395f12a46379(@types/react@18.3.18)(react@18.3.1): + minecraft-inventory-gui@https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/89c33d396f3fde4804c71f4be3c203ade1833b41(@types/react@18.3.18)(react@18.3.1): dependencies: valtio: 1.13.2(@types/react@18.3.18)(react@18.3.1) transitivePeerDependencies: diff --git a/scripts/updateGitDeps.ts b/scripts/updateGitDeps.ts new file mode 100644 index 00000000..f3f253ec --- /dev/null +++ b/scripts/updateGitDeps.ts @@ -0,0 +1,160 @@ +import fs from 'fs' +import path from 'path' +import yaml from 'yaml' +import { execSync } from 'child_process' +import { createInterface } from 'readline' + +interface LockfilePackage { + specifier: string + version: string +} + +interface Lockfile { + importers: { + '.': { + dependencies?: Record<string, LockfilePackage> + devDependencies?: Record<string, LockfilePackage> + } + } +} + +interface PackageJson { + pnpm?: { + updateConfig?: { + ignoreDependencies?: string[] + } + } +} + +async function prompt(question: string): Promise<string> { + const rl = createInterface({ + input: process.stdin, + output: process.stdout + }) + + return new Promise(resolve => { + rl.question(question, answer => { + rl.close() + resolve(answer.toLowerCase().trim()) + }) + }) +} + +async function getLatestCommit(owner: string, repo: string): Promise<string> { + const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/commits/HEAD`) + if (!response.ok) { + throw new Error(`Failed to fetch latest commit: ${response.statusText}`) + } + const data = await response.json() + return data.sha +} + +function extractGitInfo(specifier: string): { owner: string; repo: string; branch: string } | null { + const match = specifier.match(/github:([^/]+)\/([^#]+)(?:#(.+))?/) + if (!match) return null + return { + owner: match[1], + repo: match[2], + branch: match[3] || 'master' + } +} + +function extractCommitHash(version: string): string | null { + const match = version.match(/https:\/\/codeload\.github\.com\/[^/]+\/[^/]+\/tar\.gz\/([a-f0-9]+)/) + return match ? match[1] : null +} + +function getIgnoredDependencies(): string[] { + try { + const packageJsonPath = path.join(process.cwd(), 'package.json') + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as PackageJson + return packageJson.pnpm?.updateConfig?.ignoreDependencies || [] + } catch (error) { + console.warn('Failed to read package.json for ignored dependencies:', error) + return [] + } +} + +async function main() { + const lockfilePath = path.join(process.cwd(), 'pnpm-lock.yaml') + const lockfileContent = fs.readFileSync(lockfilePath, 'utf8') + const lockfile = yaml.parse(lockfileContent) as Lockfile + + const ignoredDependencies = new Set(getIgnoredDependencies()) + console.log('Ignoring dependencies:', Array.from(ignoredDependencies).join(', ') || 'none') + + const dependencies = { + ...lockfile.importers['.'].dependencies, + ...lockfile.importers['.'].devDependencies + } + + const updates: Array<{ + name: string + currentHash: string + latestHash: string + gitInfo: ReturnType<typeof extractGitInfo> + }> = [] + + console.log('\nChecking git dependencies...') + for (const [name, pkg] of Object.entries(dependencies)) { + if (ignoredDependencies.has(name)) { + console.log(`Skipping ignored dependency: ${name}`) + continue + } + + if (!pkg.specifier.startsWith('github:')) continue + + const gitInfo = extractGitInfo(pkg.specifier) + if (!gitInfo) continue + + const currentHash = extractCommitHash(pkg.version) + if (!currentHash) continue + + try { + process.stdout.write(`Checking ${name}... `) + const latestHash = await getLatestCommit(gitInfo.owner, gitInfo.repo) + if (currentHash !== latestHash) { + console.log('update available') + updates.push({ name, currentHash, latestHash, gitInfo }) + } else { + console.log('up to date') + } + } catch (error) { + console.log('failed') + console.error(`Error checking ${name}:`, error) + } + } + + if (updates.length === 0) { + console.log('\nAll git dependencies are up to date!') + return + } + + console.log('\nThe following git dependencies can be updated:') + for (const update of updates) { + console.log(`\n${update.name}:`) + console.log(` Current: ${update.currentHash}`) + console.log(` Latest: ${update.latestHash}`) + console.log(` Repo: ${update.gitInfo!.owner}/${update.gitInfo!.repo}`) + } + + const answer = await prompt('\nWould you like to update these dependencies? (y/N): ') + if (answer === 'y' || answer === 'yes') { + let newLockfileContent = lockfileContent + for (const update of updates) { + newLockfileContent = newLockfileContent.replace( + new RegExp(update.currentHash, 'g'), + update.latestHash + ) + } + fs.writeFileSync(lockfilePath, newLockfileContent) + console.log('\nUpdated pnpm-lock.yaml with new commit hashes') + console.log('Running pnpm install to apply changes...') + execSync('pnpm install', { stdio: 'inherit' }) + console.log('Done!') + } else { + console.log('\nNo changes were made.') + } +} + +main().catch(console.error) From 043e28ed975825298e97093d46d398636df490bf Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Wed, 2 Jul 2025 18:16:44 +0300 Subject: [PATCH 080/181] add more visible integration trigger --- README.MD | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.MD b/README.MD index 3205dfb4..8ca36a3b 100644 --- a/README.MD +++ b/README.MD @@ -1,3 +1,5 @@ +This project is tested with BrowserStack + # Minecraft Web Client ![banner](./docs-assets/banner.jpg) From d112b0117793a5ba121001395b1d8f9acf0006fa Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Wed, 2 Jul 2025 18:55:33 +0300 Subject: [PATCH 081/181] fix gameLoaded --- index.html | 2 +- src/appStatus.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index 6471c495..b2fa3dbd 100644 --- a/index.html +++ b/index.html @@ -63,7 +63,7 @@ if (window.navigator.maxTouchPoints > 1) window.location.hash = '#dev' // show eruda // unregister all sw - if (window.navigator.serviceWorker) { + if (window.navigator.serviceWorker && document.querySelector('.initial-loader').style.opacity !== 0) { console.log('got worker') window.navigator.serviceWorker.getRegistrations().then(registrations => { registrations.forEach(registration => { diff --git a/src/appStatus.ts b/src/appStatus.ts index 054975f3..101714f5 100644 --- a/src/appStatus.ts +++ b/src/appStatus.ts @@ -1,3 +1,4 @@ +import { resetStateAfterDisconnect } from './browserfs' import { hideModal, activeModalStack, showModal, miscUiState } from './globalState' import { appStatusState, resetAppStatusState } from './react/AppStatusProvider' @@ -25,7 +26,6 @@ export const setLoadingScreenStatus = function (status: string | undefined | nul } showModal({ reactType: 'app-status' }) if (appStatusState.isError) { - miscUiState.gameLoaded = false return } appStatusState.hideDots = hideDots @@ -33,5 +33,9 @@ export const setLoadingScreenStatus = function (status: string | undefined | nul appStatusState.lastStatus = isError ? appStatusState.status : '' appStatusState.status = status appStatusState.minecraftJsonMessage = minecraftJsonMessage ?? null + + if (isError && miscUiState.gameLoaded) { + resetStateAfterDisconnect() + } } globalThis.setLoadingScreenStatus = setLoadingScreenStatus From fcf987efe4bc5e58c5329c2ffb6ad3e49ab5bc06 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Wed, 2 Jul 2025 19:21:18 +0300 Subject: [PATCH 082/181] always display reconnect button on mcraft.fun --- .github/workflows/build-single-file.yml | 2 ++ .github/workflows/build-zip.yml | 2 ++ .github/workflows/next-deploy.yml | 1 + .github/workflows/preview.yml | 1 + .github/workflows/release.yml | 1 + config.mcraft-only.json | 3 +++ renderer/viewer/three/threeJsSound.ts | 8 +++++-- rsbuild.config.ts | 5 +++-- src/appConfig.ts | 1 + src/env.d.ts | 30 +++++++++++++++++++++++++ src/react/PauseScreen.tsx | 3 ++- 11 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 config.mcraft-only.json create mode 100644 src/env.d.ts diff --git a/.github/workflows/build-single-file.yml b/.github/workflows/build-single-file.yml index 93b1b77f..5f9800db 100644 --- a/.github/workflows/build-single-file.yml +++ b/.github/workflows/build-single-file.yml @@ -23,6 +23,8 @@ jobs: - name: Build single-file version - minecraft.html run: pnpm build-single-file && mv dist/single/index.html minecraft.html + env: + LOCAL_CONFIG_FILE: config.mcraft-only.json - name: Upload artifact uses: actions/upload-artifact@v4 diff --git a/.github/workflows/build-zip.yml b/.github/workflows/build-zip.yml index cc472476..76ca65ca 100644 --- a/.github/workflows/build-zip.yml +++ b/.github/workflows/build-zip.yml @@ -23,6 +23,8 @@ jobs: - name: Build project run: pnpm build + env: + LOCAL_CONFIG_FILE: config.mcraft-only.json - name: Bundle server.js run: | diff --git a/.github/workflows/next-deploy.yml b/.github/workflows/next-deploy.yml index 99172bf7..75b39f6c 100644 --- a/.github/workflows/next-deploy.yml +++ b/.github/workflows/next-deploy.yml @@ -36,6 +36,7 @@ jobs: run: vercel build --token=${{ secrets.VERCEL_TOKEN }} env: CONFIG_JSON_SOURCE: BUNDLED + LOCAL_CONFIG_FILE: config.mcraft-only.json - name: Copy playground files run: | mkdir -p .vercel/output/static/playground diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index f243a395..89fd6698 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -78,6 +78,7 @@ jobs: run: vercel build --token=${{ secrets.VERCEL_TOKEN }} env: CONFIG_JSON_SOURCE: BUNDLED + LOCAL_CONFIG_FILE: config.mcraft-only.json - name: Copy playground files run: | mkdir -p .vercel/output/static/playground diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 76322cb5..cbf52251 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,6 +34,7 @@ jobs: - run: vercel build --token=${{ secrets.VERCEL_TOKEN }} --prod env: CONFIG_JSON_SOURCE: BUNDLED + LOCAL_CONFIG_FILE: config.mcraft-only.json - name: Copy playground files run: | mkdir -p .vercel/output/static/playground diff --git a/config.mcraft-only.json b/config.mcraft-only.json new file mode 100644 index 00000000..51b6eee6 --- /dev/null +++ b/config.mcraft-only.json @@ -0,0 +1,3 @@ +{ + "alwaysReconnectButton": true +} diff --git a/renderer/viewer/three/threeJsSound.ts b/renderer/viewer/three/threeJsSound.ts index 627cabf8..46aefda9 100644 --- a/renderer/viewer/three/threeJsSound.ts +++ b/renderer/viewer/three/threeJsSound.ts @@ -39,7 +39,9 @@ export class ThreeJsSound implements SoundSystem { sound.position.set(position.x, position.y, position.z) sound.onEnded = () => { this.worldRenderer.scene.remove(sound) - sound.disconnect() + if (sound.source) { + sound.disconnect() + } this.activeSounds.delete(sound) audioLoader.manager.itemEnd(path) } @@ -51,7 +53,9 @@ export class ThreeJsSound implements SoundSystem { // Stop and clean up all active sounds for (const sound of this.activeSounds) { sound.stop() - sound.disconnect() + if (sound.source) { + sound.disconnect() + } } // Remove and cleanup audio listener diff --git a/rsbuild.config.ts b/rsbuild.config.ts index 8bcbd75d..5014bdf9 100644 --- a/rsbuild.config.ts +++ b/rsbuild.config.ts @@ -1,3 +1,4 @@ +/// <reference types="./src/env" /> import { defineConfig, mergeRsbuildConfig, RsbuildPluginAPI } from '@rsbuild/core' import { pluginReact } from '@rsbuild/plugin-react' import { pluginTypedCSSModules } from '@rsbuild/plugin-typed-css-modules' @@ -48,7 +49,7 @@ if (fs.existsSync('./assets/release.json')) { const configJson = JSON.parse(fs.readFileSync('./config.json', 'utf8')) try { - Object.assign(configJson, JSON.parse(fs.readFileSync('./config.local.json', 'utf8'))) + Object.assign(configJson, JSON.parse(fs.readFileSync(process.env.LOCAL_CONFIG_FILE || './config.local.json', 'utf8'))) } catch (err) {} if (dev) { configJson.defaultProxy = ':8080' @@ -198,7 +199,7 @@ const appConfig = defineConfig({ } if (configSource === 'REMOTE') { - fs.writeFileSync('./dist/config.json', JSON.stringify(configJson), 'utf8') + fs.writeFileSync('./dist/config.json', JSON.stringify(configJson, undefined, 2), 'utf8') } if (fs.existsSync('./generated/sounds.js')) { fs.copyFileSync('./generated/sounds.js', './dist/sounds.js') diff --git a/src/appConfig.ts b/src/appConfig.ts index 0805c448..b8c1e219 100644 --- a/src/appConfig.ts +++ b/src/appConfig.ts @@ -55,6 +55,7 @@ export type AppConfig = { showModsButton?: boolean defaultUsername?: string skinTexturesProxy?: string + alwaysReconnectButton?: boolean } export const loadAppConfig = (appConfig: AppConfig) => { diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 00000000..4cb5bafd --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1,30 @@ +declare namespace NodeJS { + interface ProcessEnv { + // Build configuration + NODE_ENV: 'development' | 'production' + SINGLE_FILE_BUILD?: string + DISABLE_SERVICE_WORKER?: string + CONFIG_JSON_SOURCE?: 'BUNDLED' | 'REMOTE' + LOCAL_CONFIG_FILE?: string + BUILD_VERSION?: string + + // GitHub and Vercel related + GITHUB_REPOSITORY?: string + VERCEL_GIT_REPO_OWNER?: string + VERCEL_GIT_REPO_SLUG?: string + + // UI and Features + MAIN_MENU_LINKS?: string + ENABLE_COOKIE_STORAGE?: string + COOKIE_STORAGE_PREFIX?: string + + // Release information + RELEASE_TAG?: string + RELEASE_LINK?: string + RELEASE_CHANGELOG?: string + + // Other configurations + DEPS_VERSIONS?: string + INLINED_APP_CONFIG?: string + } +} diff --git a/src/react/PauseScreen.tsx b/src/react/PauseScreen.tsx index 22e00ded..4bdd2974 100644 --- a/src/react/PauseScreen.tsx +++ b/src/react/PauseScreen.tsx @@ -163,6 +163,7 @@ export default () => { const { noConnection } = useSnapshot(gameAdditionalState) const { active: packetsReplaceActive, hasRecordedPackets: packetsReplaceHasRecordedPackets } = useSnapshot(packetsRecordingState) const { displayRecordButton: displayPacketsButtons } = useSnapshot(options) + const { appConfig } = useSnapshot(miscUiState) const handlePointerLockChange = () => { if (!pointerLock.hasPointerLock && activeModalStack.length === 0) { @@ -293,7 +294,7 @@ export default () => { {fsState.inMemorySave && !fsState.syncFs && !fsState.isReadonly ? 'Save & Quit' : 'Disconnect & Reset'} </Button> </>} - {noConnection && ( + {(noConnection || appConfig?.alwaysReconnectButton) && ( <Button className="button" style={{ width: '204px' }} onClick={reconnectReload}> Reconnect </Button> From 7162d2f549c400f9cee9282c9ce7e19baf3dbd6c Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Wed, 2 Jul 2025 19:22:47 +0300 Subject: [PATCH 083/181] fix music crash --- src/sounds/botSoundSystem.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sounds/botSoundSystem.ts b/src/sounds/botSoundSystem.ts index 0e23a98a..72aa3da8 100644 --- a/src/sounds/botSoundSystem.ts +++ b/src/sounds/botSoundSystem.ts @@ -71,7 +71,7 @@ subscribeKey(miscUiState, 'gameLoaded', async () => { } const musicStartCheck = async (force = false) => { - if (!soundMap) return + if (!soundMap || !bot) return // 20% chance to start music if (Math.random() > 0.2 && !force && !options.enableMusic) return From 9f3d3f93fb29c86793bc26f7eba8c6cd144da52a Mon Sep 17 00:00:00 2001 From: Vitaly <vital2580@icloud.com> Date: Thu, 3 Jul 2025 18:01:38 +0300 Subject: [PATCH 084/181] docs: update README to clarify BrowserStack testing (#384) --- README.MD | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.MD b/README.MD index 8ca36a3b..61a5b733 100644 --- a/README.MD +++ b/README.MD @@ -1,5 +1,3 @@ -This project is tested with BrowserStack - # Minecraft Web Client ![banner](./docs-assets/banner.jpg) @@ -47,9 +45,9 @@ All components that are in [Storybook](https://minimap.mcraft.fun/storybook/) ar ### Browser Notes -This project is tested with BrowserStack ! [BrowserStack](https://www.browserstack.com/) +This project is tested with BrowserStack. Special thanks to [BrowserStack](https://www.browserstack.com/) for providing testing infrastructure! -Howerver, these browsers have issues: +Howerver, it's known that these browsers have issues: **Opera Mini**: Disable *mouse gestures* in browsre settings to avoid opening new tab on right click hold From 4277c3a26299849848d6044e6f5429889f6519c9 Mon Sep 17 00:00:00 2001 From: Max Lee <max@themoep.de> Date: Thu, 3 Jul 2025 15:15:24 +0000 Subject: [PATCH 085/181] feat: Support nameTagVisibility option of teams (#373) --- renderer/viewer/lib/basePlayerState.ts | 4 +- renderer/viewer/lib/worldDataEmitter.ts | 1 + renderer/viewer/three/entities.ts | 23 ++++++++- src/entities.ts | 64 +++++++++++++++++++++++++ src/mineflayer/playerState.ts | 4 ++ 5 files changed, 93 insertions(+), 3 deletions(-) diff --git a/renderer/viewer/lib/basePlayerState.ts b/renderer/viewer/lib/basePlayerState.ts index f9e4b2c9..af5d9d06 100644 --- a/renderer/viewer/lib/basePlayerState.ts +++ b/renderer/viewer/lib/basePlayerState.ts @@ -1,5 +1,5 @@ import { ItemSelector } from 'mc-assets/dist/itemDefinitions' -import { GameMode } from 'mineflayer' +import { GameMode, Team } from 'mineflayer' import { proxy } from 'valtio' import type { HandItemBlock } from '../three/holdingBlock' @@ -50,6 +50,8 @@ export const getInitialPlayerState = () => proxy({ perspective: 'first_person' as CameraPerspective, cameraSpectatingEntity: undefined as number | undefined, + + team: undefined as Team | undefined, }) export const getPlayerStateUtils = (reactive: PlayerStateReactive) => ({ diff --git a/renderer/viewer/lib/worldDataEmitter.ts b/renderer/viewer/lib/worldDataEmitter.ts index 78da96d9..e1ac2f24 100644 --- a/renderer/viewer/lib/worldDataEmitter.ts +++ b/renderer/viewer/lib/worldDataEmitter.ts @@ -104,6 +104,7 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo ...e, pos: e.position, username: e.username, + team: bot.teamMap[e.username] || bot.teamMap[e.uuid], // set debugTree (obj) { // e.debugTree = obj // } diff --git a/renderer/viewer/three/entities.ts b/renderer/viewer/three/entities.ts index fd67ea80..414b0279 100644 --- a/renderer/viewer/three/entities.ts +++ b/renderer/viewer/three/entities.ts @@ -16,6 +16,7 @@ import { Item } from 'prismarine-item' import { BlockModel } from 'mc-assets' import { isEntityAttackable } from 'mineflayer-mouse/dist/attackableEntity' import { Vec3 } from 'vec3' +import { Team } from 'mineflayer' import { EntityMetadataVersions } from '../../../src/mcDataTypes' import { ItemSpecificContextProperties } from '../lib/basePlayerState' import { loadSkinImage, loadSkinFromUsername, stevePngUrl, steveTexture, createCanvas } from '../lib/utils/skins' @@ -209,7 +210,7 @@ export type SceneEntity = THREE.Object3D & { username?: string uuid?: string additionalCleanup?: () => void - originalEntity: import('prismarine-entity').Entity & { delete?; pos?, name } + originalEntity: import('prismarine-entity').Entity & { delete?; pos?, name, team?: Team } } export class Entities { @@ -900,6 +901,8 @@ export class Entities { nameTag.position.y = playerObject.position.y + playerObject.scale.y * 16 + 3 nameTag.renderOrder = 1000 + nameTag.name = 'nametag' + //@ts-expect-error wrapper.add(nameTag) } @@ -1111,10 +1114,12 @@ export class Entities { } } - if (entity.username) { + if (entity.username !== undefined) { e.username = entity.username } + this.updateNameTagVisibility(e) + this.updateEntityPosition(entity, justAdded, overrides) } @@ -1183,6 +1188,20 @@ export class Entities { } } + updateNameTagVisibility (entity: SceneEntity) { + const playerTeam = this.worldRenderer.playerStateReactive.team + const entityTeam = entity.originalEntity.team + const nameTagVisibility = entityTeam?.nameTagVisibility || 'always' + const showNameTag = nameTagVisibility === 'always' || + (nameTagVisibility === 'hideForOwnTeam' && entityTeam?.team !== playerTeam?.team) || + (nameTagVisibility === 'hideForOtherTeams' && (entityTeam?.team === playerTeam?.team || playerTeam === undefined)) + entity.traverse(c => { + if (c.name === 'nametag') { + c.visible = showNameTag + } + }) + } + addMapModel (entityMesh: THREE.Object3D, mapNumber: number, rotation: number) { const imageData = this.cachedMapsImages?.[mapNumber] let texture: THREE.Texture | null = null diff --git a/src/entities.ts b/src/entities.ts index 2ff51a48..3dac51ce 100644 --- a/src/entities.ts +++ b/src/entities.ts @@ -4,6 +4,7 @@ import tracker from '@nxg-org/mineflayer-tracker' import { loader as autoJumpPlugin } from '@nxg-org/mineflayer-auto-jump' import { subscribeKey } from 'valtio/utils' import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods' +import { Team } from 'mineflayer' import { options, watchValue } from './optionsStorage' import { gameAdditionalState, miscUiState } from './globalState' import { EntityStatus } from './mineflayer/entityStatus' @@ -237,4 +238,67 @@ customEvents.on('gameLoaded', () => { } }) + + bot.on('teamUpdated', (team: Team) => { + for (const entity of Object.values(bot.entities)) { + if (entity.type === 'player' && entity.username && team.members.includes(entity.username) || entity.uuid && team.members.includes(entity.uuid)) { + bot.emit('entityUpdate', entity) + } + } + }) + + const updateEntityNameTags = (team: Team) => { + for (const entity of Object.values(bot.entities)) { + const entityTeam = entity.type === 'player' && entity.username ? bot.teamMap[entity.username] : entity.uuid ? bot.teamMap[entity.uuid] : undefined + if ((entityTeam?.nameTagVisibility === 'hideForOwnTeam' && entityTeam.name === team.name) + || (entityTeam?.nameTagVisibility === 'hideForOtherTeams' && entityTeam.name !== team.name)) { + bot.emit('entityUpdate', entity) + } + } + } + + const doEntitiesNeedUpdating = (team: Team) => { + return team.nameTagVisibility === 'never' + || (team.nameTagVisibility === 'hideForOtherTeams' && appViewer.playerState.reactive.team?.team !== team.team) + || (team.nameTagVisibility === 'hideForOwnTeam' && appViewer.playerState.reactive.team?.team === team.team) + } + + bot.on('teamMemberAdded', (team: Team, members: string[]) => { + if (members.includes(bot.username) && appViewer.playerState.reactive.team?.team !== team.team) { + appViewer.playerState.reactive.team = team + // Player was added to a team, need to check if any entities need updating + updateEntityNameTags(team) + } else if (doEntitiesNeedUpdating(team)) { + // Need to update all entities that were added + for (const entity of Object.values(bot.entities)) { + if (entity.type === 'player' && entity.username && members.includes(entity.username) || entity.uuid && members.includes(entity.uuid)) { + bot.emit('entityUpdate', entity) + } + } + } + }) + + bot.on('teamMemberRemoved', (team: Team, members: string[]) => { + if (members.includes(bot.username) && appViewer.playerState.reactive.team?.team === team.team) { + appViewer.playerState.reactive.team = undefined + // Player was removed from a team, need to check if any entities need updating + updateEntityNameTags(team) + } else if (doEntitiesNeedUpdating(team)) { + // Need to update all entities that were removed + for (const entity of Object.values(bot.entities)) { + if (entity.type === 'player' && entity.username && members.includes(entity.username) || entity.uuid && members.includes(entity.uuid)) { + bot.emit('entityUpdate', entity) + } + } + } + }) + + bot.on('teamRemoved', (team: Team) => { + if (appViewer.playerState.reactive.team?.team === team.team) { + appViewer.playerState.reactive.team = undefined + // Player's team was removed, need to update all entities that are in a team + updateEntityNameTags(team) + } + }) + }) diff --git a/src/mineflayer/playerState.ts b/src/mineflayer/playerState.ts index b7b4d2bd..b8919a14 100644 --- a/src/mineflayer/playerState.ts +++ b/src/mineflayer/playerState.ts @@ -99,6 +99,10 @@ export class PlayerStateControllerMain { }) this.reactive.gameMode = bot.game?.gameMode + customEvents.on('gameLoaded', () => { + this.reactive.team = bot.teamMap[bot.username] + }) + this.watchReactive() } From cdd23bc6a6a370eca0d9da79bee762af81b78ff1 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Thu, 3 Jul 2025 18:16:21 +0300 Subject: [PATCH 086/181] fix(critical): fix support for all versions above 1.20.2 in mineflayer dependency --- pnpm-lock.yaml | 14 +++++++------- scripts/updateGitDeps.ts | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b146bd80..291cec39 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -136,7 +136,7 @@ importers: version: 4.17.21 mcraft-fun-mineflayer: specifier: ^0.1.23 - version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/1616261f727398d0b14359262726828d24797fcc(encoding@0.1.13)) + version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/2cd1e3da257a0c0415e9dc5524fb14bd5844f0d8(encoding@0.1.13)) minecraft-data: specifier: 3.92.0 version: 3.92.0 @@ -341,7 +341,7 @@ importers: version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/89c33d396f3fde4804c71f4be3c203ade1833b41(@types/react@18.3.18)(react@18.3.1) mineflayer: specifier: github:zardoy/mineflayer#gen-the-master - version: https://codeload.github.com/zardoy/mineflayer/tar.gz/1616261f727398d0b14359262726828d24797fcc(encoding@0.1.13) + version: https://codeload.github.com/zardoy/mineflayer/tar.gz/2cd1e3da257a0c0415e9dc5524fb14bd5844f0d8(encoding@0.1.13) mineflayer-mouse: specifier: ^0.1.11 version: 0.1.11 @@ -6678,8 +6678,8 @@ packages: resolution: {integrity: sha512-GtW4hkijyZbSu5LKYYD89xZu+XY7OoP7IkrCnNEn6EdPm0+vr2THoJgFGKrlze9/81+T+P3E4qvJXNFiU/zeJg==} engines: {node: '>=22'} - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/1616261f727398d0b14359262726828d24797fcc: - resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/1616261f727398d0b14359262726828d24797fcc} + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/2cd1e3da257a0c0415e9dc5524fb14bd5844f0d8: + resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/2cd1e3da257a0c0415e9dc5524fb14bd5844f0d8} version: 4.30.0 engines: {node: '>=22'} @@ -16956,12 +16956,12 @@ snapshots: maxrects-packer: '@zardoy/maxrects-packer@2.7.4' zod: 3.24.2 - mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/1616261f727398d0b14359262726828d24797fcc(encoding@0.1.13)): + mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/2cd1e3da257a0c0415e9dc5524fb14bd5844f0d8(encoding@0.1.13)): dependencies: '@zardoy/flying-squid': 0.0.49(encoding@0.1.13) exit-hook: 2.2.1 minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0)(encoding@0.1.13) - mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/1616261f727398d0b14359262726828d24797fcc(encoding@0.1.13) + mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/2cd1e3da257a0c0415e9dc5524fb14bd5844f0d8(encoding@0.1.13) prismarine-item: 1.16.0 ws: 8.18.1 transitivePeerDependencies: @@ -17379,7 +17379,7 @@ snapshots: - encoding - supports-color - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/1616261f727398d0b14359262726828d24797fcc(encoding@0.1.13): + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/2cd1e3da257a0c0415e9dc5524fb14bd5844f0d8(encoding@0.1.13): dependencies: '@nxg-org/mineflayer-physics-util': 1.8.10 minecraft-data: 3.92.0 diff --git a/scripts/updateGitDeps.ts b/scripts/updateGitDeps.ts index f3f253ec..797aea8f 100644 --- a/scripts/updateGitDeps.ts +++ b/scripts/updateGitDeps.ts @@ -149,8 +149,8 @@ async function main() { } fs.writeFileSync(lockfilePath, newLockfileContent) console.log('\nUpdated pnpm-lock.yaml with new commit hashes') - console.log('Running pnpm install to apply changes...') - execSync('pnpm install', { stdio: 'inherit' }) + // console.log('Running pnpm install to apply changes...') + // execSync('pnpm install', { stdio: 'inherit' }) console.log('Done!') } else { console.log('\nNo changes were made.') From 5cfd301d10890d37ff9cc804774fa9b485c7967d Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Thu, 3 Jul 2025 19:35:19 +0300 Subject: [PATCH 087/181] fix: fix controls debug was not visible --- src/optionsGuiScheme.tsx | 12 ------------ src/react/ControDebug.tsx | 32 ++++++++++++++++++-------------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx index ba52e333..b03db37d 100644 --- a/src/optionsGuiScheme.tsx +++ b/src/optionsGuiScheme.tsx @@ -545,18 +545,6 @@ export const guiOptionsScheme: { /> } }, - { - custom () { - const { cookieStorage } = useSnapshot(appStorage) - return <Button - label={`Storage: ${cookieStorage ? 'Synced Cookies' : 'Local Storage'}`} onClick={() => { - appStorage.cookieStorage = !cookieStorage - alert('Reload the page to apply this change') - }} - inScreen - /> - } - }, { custom () { return <Category>Server Connection</Category> diff --git a/src/react/ControDebug.tsx b/src/react/ControDebug.tsx index 9c5f272c..ca096707 100644 --- a/src/react/ControDebug.tsx +++ b/src/react/ControDebug.tsx @@ -50,20 +50,24 @@ export default () => { if (!options.debugContro) return null return ( - <div style={{ - position: 'fixed', - right: 0, - top: '50%', - transform: 'translateY(-50%)', - backgroundColor: 'rgba(0, 0, 0, 0.5)', - padding: '8px', - fontFamily: 'monospace', - fontSize: '8px', - color: 'white', - display: 'flex', - flexDirection: 'column', - gap: '4px' - }}> + <div + className='debug-contro' + style={{ + position: 'fixed', + right: 0, + top: '50%', + transform: 'translateY(-50%)', + backgroundColor: 'rgba(0, 0, 0, 0.5)', + padding: '8px', + fontFamily: 'monospace', + fontSize: '8px', + color: 'white', + display: 'flex', + flexDirection: 'column', + gap: '4px', + zIndex: 2, + }} + > <div>Keys: {[...pressedKeys].join(', ')}</div> <div style={{ color: 'limegreen' }}>Actions: {actions.join(', ')}</div> </div> From e8b0a34c0ba3ec43485bcaeb34692dd6177bde4d Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Thu, 3 Jul 2025 19:52:49 +0300 Subject: [PATCH 088/181] fix: fix chat visual text alignment issues on chat opening --- src/react/Chat.css | 9 ++++++++- src/react/Chat.tsx | 3 ++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/react/Chat.css b/src/react/Chat.css index 47394948..1f29a59c 100644 --- a/src/react/Chat.css +++ b/src/react/Chat.css @@ -4,6 +4,8 @@ div.chat-wrapper { /* z-index: 10; */ padding-left: calc(env(safe-area-inset-left) / 2); padding-right: calc(env(safe-area-inset-right, 4px) / 2); + box-sizing: content-box; + overflow: hidden; } .chat-messages-wrapper { @@ -11,12 +13,17 @@ div.chat-wrapper { padding: 4px; padding-left: 0; max-height: var(--chatHeight); - width: var(--chatWidth); + width: calc(var(--chatWidth) - 5px); /* Custom scrollbar width */ transform-origin: bottom left; transform: scale(var(--chatScale)); pointer-events: none; } +/* Restore full width when chat is opened */ +.chat-messages-wrapper.chat-opened { + width: var(--chatWidth); +} + .chat-input-wrapper { bottom: 1px; width: calc(100% - 3px); diff --git a/src/react/Chat.tsx b/src/react/Chat.tsx index 7c1c8633..2bfe94db 100644 --- a/src/react/Chat.tsx +++ b/src/react/Chat.tsx @@ -18,6 +18,7 @@ const MessageLine = ({ message, currentPlayerName, chatOpened }: { message: Mess const [fadeState, setFadeState] = useState<'visible' | 'fading' | 'faded'>('visible') useEffect(() => { + if (window.debugStopChatFade) return // Start fading after 5 seconds const fadeTimeout = setTimeout(() => { setFadeState('fading') @@ -324,7 +325,7 @@ export default ({ return ( <> <div - className={`chat-wrapper chat-messages-wrapper ${usingTouch ? 'display-mobile' : ''}`} style={{ + className={`chat-wrapper chat-messages-wrapper ${usingTouch ? 'display-mobile' : ''} ${opened ? 'chat-opened' : ''}`} style={{ userSelect: opened && allowSelection ? 'text' : undefined, }} > From 01567ea589c8501956c362200ad2ca74744bf8fd Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky <vital2580@icloud.com> Date: Fri, 4 Jul 2025 01:44:26 +0300 Subject: [PATCH 089/181] add "mcraft.fun/debug-inputs.html" --- assets/debug-inputs.html | 237 +++++++++++++++++++++++++++++++++++++++ rsbuild.config.ts | 1 + 2 files changed, 238 insertions(+) create mode 100644 assets/debug-inputs.html diff --git a/assets/debug-inputs.html b/assets/debug-inputs.html new file mode 100644 index 00000000..584fe4d7 --- /dev/null +++ b/assets/debug-inputs.html @@ -0,0 +1,237 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Web Input Debugger + + + +
+ +
+ +
+
W
+
A
+
S
+
D
+
+ +
+
Ctrl
+
+ +
+
Space
+
+ + + + diff --git a/rsbuild.config.ts b/rsbuild.config.ts index 5014bdf9..219fe57e 100644 --- a/rsbuild.config.ts +++ b/rsbuild.config.ts @@ -193,6 +193,7 @@ const appConfig = defineConfig({ fs.copyFileSync('./assets/playground.html', './dist/playground.html') fs.copyFileSync('./assets/manifest.json', './dist/manifest.json') fs.copyFileSync('./assets/config.html', './dist/config.html') + fs.copyFileSync('./assets/debug-inputs.html', './dist/debug-inputs.html') fs.copyFileSync('./assets/loading-bg.jpg', './dist/loading-bg.jpg') if (fs.existsSync('./assets/release.json')) { fs.copyFileSync('./assets/release.json', './dist/release.json') From 45bc76d825ea656a9154d11bfd54a1323a02891a Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Fri, 4 Jul 2025 17:29:55 +0300 Subject: [PATCH 090/181] hotfix(chat): fix all annoying issues on mobile. fix chat was not visible on mobile at all! --- src/react/Chat.css | 11 +++++- src/react/Chat.tsx | 87 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 91 insertions(+), 7 deletions(-) diff --git a/src/react/Chat.css b/src/react/Chat.css index 1f29a59c..246f17b3 100644 --- a/src/react/Chat.css +++ b/src/react/Chat.css @@ -5,6 +5,10 @@ div.chat-wrapper { padding-left: calc(env(safe-area-inset-left) / 2); padding-right: calc(env(safe-area-inset-right, 4px) / 2); box-sizing: content-box; +} + +/* Only apply overflow hidden when not in mobile mode */ +div.chat-wrapper:not(.display-mobile):not(.input-mobile) { overflow: hidden; } @@ -69,7 +73,7 @@ div.chat-wrapper { top: 100%; padding-left: calc(env(safe-area-inset-left) / 2); margin-top: 14px; - margin-left: 20px; + margin-left: 40px; /* input height */ } @@ -116,6 +120,11 @@ div.chat-wrapper { justify-content: flex-start; } +.input-mobile .chat-completions-items > div { + padding: 4px 0; + font-size: 10px; +} + .input-mobile { top: 15px; position: absolute; diff --git a/src/react/Chat.tsx b/src/react/Chat.tsx index 2bfe94db..a45d7a69 100644 --- a/src/react/Chat.tsx +++ b/src/react/Chat.tsx @@ -112,7 +112,9 @@ export default ({ const sendHistoryRef = useRef(JSON.parse(window.sessionStorage.chatHistory || '[]')) const [isInputFocused, setIsInputFocused] = useState(false) - const spellCheckEnabled = false + const [spellCheckEnabled, setSpellCheckEnabled] = useState(false) + const [preservedInputValue, setPreservedInputValue] = useState('') + const [inputKey, setInputKey] = useState(0) const pingHistoryRef = useRef(JSON.parse(window.localStorage.pingHistory || '[]')) const [completePadText, setCompletePadText] = useState('') @@ -237,12 +239,16 @@ export default ({ if (opened) { completeRequestValue.current = '' resetCompletionItems() + } else { + setPreservedInputValue('') } }, [opened]) const onMainInputChange = () => { const lastWord = chatInput.current.value.slice(0, chatInput.current.selectionEnd ?? chatInput.current.value.length).split(' ').at(-1)! - if (lastWord.startsWith('@') && getPingComplete) { + const isCommand = chatInput.current.value.startsWith('/') + + if (lastWord.startsWith('@') && getPingComplete && !isCommand) { setCompletePadText(lastWord) void fetchPingCompletions(true, lastWord.slice(1)) return @@ -322,6 +328,29 @@ export default ({ return completeValue } + const handleSlashCommand = () => { + remountInput('/') + } + + const handleAcceptFirstCompletion = () => { + if (completionItems.length > 0) { + acceptComplete(completionItems[0]) + } + } + + const remountInput = (newValue?: string) => { + if (newValue !== undefined) { + setPreservedInputValue(newValue) + } + setInputKey(k => k + 1) + } + + useEffect(() => { + if (preservedInputValue && chatInput.current) { + chatInput.current.focus() + } + }, [inputKey]) // Changed from spellCheckEnabled to inputKey + return ( <>
From a49877870325b010160c3e9e3dfca5719b8552f8 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Fri, 18 Jul 2025 09:56:50 +0300 Subject: [PATCH 115/181] always wait for config load so autoConnect works on remote config --- src/index.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index 7a553ca7..8de98894 100644 --- a/src/index.ts +++ b/src/index.ts @@ -982,7 +982,6 @@ const maybeEnterGame = () => { } if (appQueryParams.ip || appQueryParams.proxy) { - const waitAppConfigLoad = !appQueryParams.proxy const openServerAction = () => { if (appQueryParams.autoConnect && miscUiState.appConfig?.allowAutoConnect) { void connect({ @@ -1003,11 +1002,7 @@ const maybeEnterGame = () => { } // showModal({ reactType: 'empty' }) - if (waitAppConfigLoad) { - return waitForConfigFsLoad(openServerAction) - } - openServerAction() - return + return waitForConfigFsLoad(openServerAction) } if (appQueryParams.connectPeer) { From 4d7e3df8597fffea6a66f7a6df4810dae1d8cd96 Mon Sep 17 00:00:00 2001 From: Max Lee Date: Fri, 18 Jul 2025 11:18:05 +0000 Subject: [PATCH 116/181] feat: Item projectiles support (#395) --- pnpm-lock.yaml | 14 +++--- renderer/viewer/three/entities.ts | 72 ++++++++++++++++----------- renderer/viewer/three/holdingBlock.ts | 2 +- 3 files changed, 50 insertions(+), 38 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1e6fddf3..52c2ff30 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -136,7 +136,7 @@ importers: version: 4.17.21 mcraft-fun-mineflayer: specifier: ^0.1.23 - version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/3daf1f4bdc6afad0dedd87b879875f3dbb7b0980(encoding@0.1.13)) + version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/26c04c26f8eed2499a7c56eb4664649f3f54b767(encoding@0.1.13)) minecraft-data: specifier: 3.92.0 version: 3.92.0 @@ -341,7 +341,7 @@ importers: version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/89c33d396f3fde4804c71f4be3c203ade1833b41(@types/react@18.3.18)(react@18.3.1) mineflayer: specifier: github:zardoy/mineflayer#gen-the-master - version: https://codeload.github.com/zardoy/mineflayer/tar.gz/3daf1f4bdc6afad0dedd87b879875f3dbb7b0980(encoding@0.1.13) + version: https://codeload.github.com/zardoy/mineflayer/tar.gz/26c04c26f8eed2499a7c56eb4664649f3f54b767(encoding@0.1.13) mineflayer-mouse: specifier: ^0.1.14 version: 0.1.14 @@ -6678,8 +6678,8 @@ packages: resolution: {integrity: sha512-GtW4hkijyZbSu5LKYYD89xZu+XY7OoP7IkrCnNEn6EdPm0+vr2THoJgFGKrlze9/81+T+P3E4qvJXNFiU/zeJg==} engines: {node: '>=22'} - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/3daf1f4bdc6afad0dedd87b879875f3dbb7b0980: - resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/3daf1f4bdc6afad0dedd87b879875f3dbb7b0980} + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/26c04c26f8eed2499a7c56eb4664649f3f54b767: + resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/26c04c26f8eed2499a7c56eb4664649f3f54b767} version: 4.30.0 engines: {node: '>=22'} @@ -16956,12 +16956,12 @@ snapshots: maxrects-packer: '@zardoy/maxrects-packer@2.7.4' zod: 3.24.2 - mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/3daf1f4bdc6afad0dedd87b879875f3dbb7b0980(encoding@0.1.13)): + mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/26c04c26f8eed2499a7c56eb4664649f3f54b767(encoding@0.1.13)): dependencies: '@zardoy/flying-squid': 0.0.49(encoding@0.1.13) exit-hook: 2.2.1 minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0)(encoding@0.1.13) - mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/3daf1f4bdc6afad0dedd87b879875f3dbb7b0980(encoding@0.1.13) + mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/26c04c26f8eed2499a7c56eb4664649f3f54b767(encoding@0.1.13) prismarine-item: 1.16.0 ws: 8.18.1 transitivePeerDependencies: @@ -17379,7 +17379,7 @@ snapshots: - encoding - supports-color - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/3daf1f4bdc6afad0dedd87b879875f3dbb7b0980(encoding@0.1.13): + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/26c04c26f8eed2499a7c56eb4664649f3f54b767(encoding@0.1.13): dependencies: '@nxg-org/mineflayer-physics-util': 1.8.10 minecraft-data: 3.92.0 diff --git a/renderer/viewer/three/entities.ts b/renderer/viewer/three/entities.ts index 51292d2f..6c6f8900 100644 --- a/renderer/viewer/three/entities.ts +++ b/renderer/viewer/three/entities.ts @@ -717,7 +717,7 @@ export class Entities { return typeof component === 'string' ? component : component.text ?? '' } - getItemMesh (item, specificProps: ItemSpecificContextProperties, previousModel?: string) { + getItemMesh (item, specificProps: ItemSpecificContextProperties, faceCamera = false, previousModel?: string) { if (!item.nbt && item.nbtData) item.nbt = item.nbtData const textureUv = this.worldRenderer.getItemRenderData(item, specificProps) if (previousModel && previousModel === textureUv?.modelName) return undefined @@ -757,26 +757,37 @@ export class Entities { itemsTexture.needsUpdate = true itemsTexture.magFilter = THREE.NearestFilter itemsTexture.minFilter = THREE.NearestFilter - const itemsTextureFlipped = itemsTexture.clone() - itemsTextureFlipped.repeat.x *= -1 - itemsTextureFlipped.needsUpdate = true - itemsTextureFlipped.offset.set(u + (sizeX), 1 - v - sizeY) - const material = new THREE.MeshStandardMaterial({ - map: itemsTexture, - transparent: true, - alphaTest: 0.1, - }) - const materialFlipped = new THREE.MeshStandardMaterial({ - map: itemsTextureFlipped, - transparent: true, - alphaTest: 0.1, - }) - const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 0), [ - // top left and right bottom are black box materials others are transparent - new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), - new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), - material, materialFlipped, - ]) + let mesh: THREE.Object3D + let itemsTextureFlipped: THREE.Texture | undefined + if (faceCamera) { + const spriteMat = new THREE.SpriteMaterial({ + map: itemsTexture, + transparent: true, + alphaTest: 0.1, + }) + mesh = new THREE.Sprite(spriteMat) + } else { + itemsTextureFlipped = itemsTexture.clone() + itemsTextureFlipped.repeat.x *= -1 + itemsTextureFlipped.needsUpdate = true + itemsTextureFlipped.offset.set(u + (sizeX), 1 - v - sizeY) + const material = new THREE.MeshStandardMaterial({ + map: itemsTexture, + transparent: true, + alphaTest: 0.1, + }) + const materialFlipped = new THREE.MeshStandardMaterial({ + map: itemsTextureFlipped, + transparent: true, + alphaTest: 0.1, + }) + mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 0), [ + // top left and right bottom are black box materials others are transparent + new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), + new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), + material, materialFlipped, + ]) + } let SCALE = 1 if (specificProps['minecraft:display_context'] === 'ground') { SCALE = 0.5 @@ -805,8 +816,6 @@ export class Entities { } update (entity: SceneEntity['originalEntity'], overrides) { - const justAdded = !this.entities[entity.id] - const isPlayerModel = entity.name === 'player' if (entity.name === 'zombie_villager' || entity.name === 'husk') { overrides.texture = `textures/1.16.4/entity/${entity.name === 'zombie_villager' ? 'zombie_villager/zombie_villager.png' : `zombie/${entity.name}.png`}` @@ -817,6 +826,7 @@ export class Entities { } // this can be undefined in case where packet entity_destroy was sent twice (so it was already deleted) let e = this.entities[entity.id] + const justAdded = !e if (entity.delete) { if (!e) return @@ -836,21 +846,23 @@ export class Entities { if (e === undefined) { const group = new THREE.Group() as unknown as SceneEntity group.originalEntity = entity - if (entity.name === 'item' || entity.name === 'tnt' || entity.name === 'falling_block') { - const item = entity.name === 'tnt' - ? { name: 'tnt' } + if (entity.name === 'item' || entity.name === 'tnt' || entity.name === 'falling_block' || entity.name === 'snowball' + || entity.name === 'egg' || entity.name === 'ender_pearl' || entity.name === 'experience_bottle' + || entity.name === 'splash_potion' || entity.name === 'lingering_potion') { + const item = entity.name === 'tnt' || entity.type === 'projectile' + ? { name: entity.name } : entity.name === 'falling_block' ? { blockState: entity['objectData'] } : entity.metadata?.find((m: any) => typeof m === 'object' && m?.itemCount) if (item) { const object = this.getItemMesh(item, { 'minecraft:display_context': 'ground', - }) + }, entity.type === 'projectile') if (object) { mesh = object.mesh - if (entity.name === 'item') { + if (entity.name === 'item' || entity.type === 'projectile') { mesh.scale.set(0.5, 0.5, 0.5) - mesh.position.set(0, 0.2, 0) + mesh.position.set(0, entity.name === 'item' ? 0.2 : 0.1, 0) } else { mesh.scale.set(2, 2, 2) mesh.position.set(0, 0.5, 0) @@ -858,8 +870,8 @@ export class Entities { // set faces // mesh.position.set(targetPos.x + 0.5 + 2, targetPos.y + 0.5, targetPos.z + 0.5) // viewer.scene.add(mesh) - const clock = new THREE.Clock() if (entity.name === 'item') { + const clock = new THREE.Clock() mesh.onBeforeRender = () => { const delta = clock.getDelta() mesh!.rotation.y += delta diff --git a/renderer/viewer/three/holdingBlock.ts b/renderer/viewer/three/holdingBlock.ts index c8a56386..f9d00f0e 100644 --- a/renderer/viewer/three/holdingBlock.ts +++ b/renderer/viewer/three/holdingBlock.ts @@ -357,7 +357,7 @@ export default class HoldingBlock { 'minecraft:display_context': 'firstperson', 'minecraft:use_duration': this.worldRenderer.playerStateReactive.itemUsageTicks, 'minecraft:using_item': !!this.worldRenderer.playerStateReactive.itemUsageTicks, - }, this.lastItemModelName) + }, false, this.lastItemModelName) if (result) { const { mesh: itemMesh, isBlock, modelName } = result if (isBlock) { From b9c8ade9bf05b3eea2842bcfe2e03bb7428cfafb Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sun, 20 Jul 2025 10:06:57 +0300 Subject: [PATCH 117/181] fix: fix chat was crashing sometimes --- src/index.ts | 4 ++++ src/react/Chat.tsx | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 8de98894..d28261f4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -278,6 +278,10 @@ export async function connect (connectOptions: ConnectOptions) { return } } + if (e.reason?.stack?.includes('chrome-extension://')) { + // ignore issues caused by chrome extension + return + } handleError(e.reason) }, { signal: errorAbortController.signal diff --git a/src/react/Chat.tsx b/src/react/Chat.tsx index a45d7a69..20d77a10 100644 --- a/src/react/Chat.tsx +++ b/src/react/Chat.tsx @@ -45,7 +45,7 @@ const MessageLine = ({ message, currentPlayerName, chatOpened }: { message: Mess return
  • val).map(([name]) => name).join(' ')} data-time={message.timestamp ? new Date(message.timestamp).toLocaleString('en-US', { hour12: false }) : undefined}> {message.parts.map((msg, i) => { // Check if this is a text part that might contain a mention - if (msg.text && currentPlayerName) { + if (typeof msg.text === 'string' && currentPlayerName) { const parts = msg.text.split(new RegExp(`(@${currentPlayerName})`, 'i')) if (parts.length > 1) { return parts.map((txtPart, j) => { From 67855ae25a97d00a5206299963794bf223e2eebe Mon Sep 17 00:00:00 2001 From: Kesu <40177436+Kesuaheli@users.noreply.github.com> Date: Sun, 27 Jul 2025 15:24:26 +0200 Subject: [PATCH 118/181] fix: fix some window titles (#401) --- src/inventoryWindows.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/inventoryWindows.ts b/src/inventoryWindows.ts index 1e17db14..bc7dcbaf 100644 --- a/src/inventoryWindows.ts +++ b/src/inventoryWindows.ts @@ -9,6 +9,7 @@ import PItem, { Item } from 'prismarine-item' import { versionToNumber } from 'renderer/viewer/common/utils' import { getRenamedData } from 'flying-squid/dist/blockRenames' import PrismarineChatLoader from 'prismarine-chat' +import * as nbt from 'prismarine-nbt' import { BlockModel } from 'mc-assets' import { renderSlot } from 'renderer/viewer/three/renderSlot' import Generic95 from '../assets/generic_95.png' @@ -59,9 +60,9 @@ export const onGameLoad = () => { bot.on('windowOpen', (win) => { const implementedWindow = implementedContainersGuiMap[mapWindowType(win.type as string, win.inventoryStart)] if (implementedWindow) { - openWindow(implementedWindow) + openWindow(implementedWindow, nbt.simplify(win.title as any)) } else if (options.unimplementedContainers) { - openWindow('ChestWin') + openWindow('ChestWin', nbt.simplify(win.title as any)) } else { // todo format displayClientChat(`[client error] cannot open unimplemented window ${win.id} (${win.type}). Slots: ${win.slots.map(item => getItemName(item)).filter(Boolean).join(', ')}`) @@ -354,7 +355,7 @@ const upWindowItemsLocal = () => { } let skipClosePacketSending = false -const openWindow = (type: string | undefined) => { +const openWindow = (type: string | undefined, title: string | any = undefined) => { // if (activeModalStack.some(x => x.reactType?.includes?.('player_win:'))) { if (activeModalStack.length) { // game is not in foreground, don't close current modal if (type) { @@ -384,7 +385,6 @@ const openWindow = (type: string | undefined) => { const inv = openItemsCanvas(type) inv.canvasManager.children[0].mobileHelpers = miscUiState.currentTouch window.inventory = inv - const title = bot.currentWindow?.title const PrismarineChat = PrismarineChatLoader(bot.version) try { inv.canvasManager.children[0].customTitleText = title ? From c4b284b9b7b644fc0c7eee2a7231752df499b452 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Sat, 2 Aug 2025 21:34:33 +0300 Subject: [PATCH 119/181] fix: fix supported versions display in server menu --- src/react/ServersListProvider.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/react/ServersListProvider.tsx b/src/react/ServersListProvider.tsx index 2509db74..75f95d3f 100644 --- a/src/react/ServersListProvider.tsx +++ b/src/react/ServersListProvider.tsx @@ -1,9 +1,11 @@ import { useEffect, useMemo, useState } from 'react' import { useUtilsEffect } from '@zardoy/react-util' import { useSnapshot } from 'valtio' +import { supportedVersions } from 'minecraft-protocol' +import { versionToNumber } from 'mc-assets/dist/utils' import { ConnectOptions } from '../connect' import { activeModalStack, hideCurrentModal, miscUiState, notHideableModalsWithoutForce, showModal } from '../globalState' -import supportedVersions from '../supportedVersions.mjs' +import appSupportedVersions from '../supportedVersions.mjs' import { appQueryParams } from '../appParams' import { fetchServerStatus, isServerValid } from '../api/mcStatusApi' import { getServerInfo } from '../mineflayer/mc-protocol' @@ -20,6 +22,10 @@ import Button from './Button' import { pixelartIcons } from './PixelartIcon' import { showNotification } from './NotificationProvider' +const firstProtocolVersion = versionToNumber(supportedVersions[0]) +const lastProtocolVersion = versionToNumber(supportedVersions.at(-1)!) +const protocolSupportedVersions = appSupportedVersions.filter(v => versionToNumber(v) >= firstProtocolVersion && versionToNumber(v) <= lastProtocolVersion) + const EXPLICIT_SHARE_SERVER_MODE = false if (appQueryParams.lockConnect) { @@ -262,7 +268,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL } dispatchEvent(new CustomEvent('connect', { detail: connectOptions })) }} - versions={supportedVersions} + versions={protocolSupportedVersions} /> : null const serversListJsx = Date: Sun, 3 Aug 2025 03:20:38 +0300 Subject: [PATCH 120/181] fix: up protocol to support 1.21.5 --- patches/minecraft-protocol.patch | 110 +++++++++++++++++-------------- pnpm-lock.yaml | 77 +++++++++++----------- 2 files changed, 101 insertions(+), 86 deletions(-) diff --git a/patches/minecraft-protocol.patch b/patches/minecraft-protocol.patch index 6a82d5cf..108085a1 100644 --- a/patches/minecraft-protocol.patch +++ b/patches/minecraft-protocol.patch @@ -1,8 +1,8 @@ diff --git a/src/client/chat.js b/src/client/chat.js -index 8d0869b150681574ad19292a026cce9f67a137ee..2efa2e6600f017b566155974cb9fb1856fa582f9 100644 +index a50f4b988ad9fb29d5eb9e1633b498615aa9cd28..b8b819eef0762a77d9db5c0fb06be648303628c7 100644 --- a/src/client/chat.js +++ b/src/client/chat.js -@@ -109,7 +109,7 @@ module.exports = function (client, options) { +@@ -110,7 +110,7 @@ module.exports = function (client, options) { for (const player of packet.data) { if (player.chatSession) { client._players[player.uuid] = { @@ -11,8 +11,8 @@ index 8d0869b150681574ad19292a026cce9f67a137ee..2efa2e6600f017b566155974cb9fb185 publicKeyDER: player.chatSession.publicKey.keyBytes, sessionUuid: player.chatSession.uuid } -@@ -119,7 +119,7 @@ module.exports = function (client, options) { - +@@ -120,7 +120,7 @@ module.exports = function (client, options) { + if (player.crypto) { client._players[player.uuid] = { - publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }), @@ -20,7 +20,7 @@ index 8d0869b150681574ad19292a026cce9f67a137ee..2efa2e6600f017b566155974cb9fb185 publicKeyDER: player.crypto.publicKey, signature: player.crypto.signature, displayName: player.displayName || player.name -@@ -189,7 +189,7 @@ module.exports = function (client, options) { +@@ -190,7 +190,7 @@ module.exports = function (client, options) { if (mcData.supportFeature('useChatSessions')) { const tsDelta = BigInt(Date.now()) - packet.timestamp const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0 @@ -28,26 +28,26 @@ index 8d0869b150681574ad19292a026cce9f67a137ee..2efa2e6600f017b566155974cb9fb185 + const verified = false && !packet.unsignedChatContent && updateAndValidateSession(packet.senderUuid, packet.plainMessage, packet.signature, packet.index, packet.previousMessages, packet.salt, packet.timestamp) && !expired if (verified) client._signatureCache.push(packet.signature) client.emit('playerChat', { - plainMessage: packet.plainMessage, -@@ -354,7 +354,7 @@ module.exports = function (client, options) { + globalIndex: packet.globalIndex, +@@ -356,7 +356,7 @@ module.exports = function (client, options) { } } - + - client._signedChat = (message, options = {}) => { + client._signedChat = async (message, options = {}) => { options.timestamp = options.timestamp || BigInt(Date.now()) options.salt = options.salt || 1n - -@@ -396,7 +396,7 @@ module.exports = function (client, options) { + +@@ -401,7 +401,7 @@ module.exports = function (client, options) { message, timestamp: options.timestamp, salt: options.salt, - signature: (client.profileKeys && client._session) ? client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined, + signature: (client.profileKeys && client._session) ? await client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined, offset: client._lastSeenMessages.pending, + checksum: computeChatChecksum(client._lastSeenMessages), // 1.21.5+ acknowledged - }) -@@ -410,7 +410,7 @@ module.exports = function (client, options) { +@@ -416,7 +416,7 @@ module.exports = function (client, options) { message, timestamp: options.timestamp, salt: options.salt, @@ -57,7 +57,7 @@ index 8d0869b150681574ad19292a026cce9f67a137ee..2efa2e6600f017b566155974cb9fb185 previousMessages: client._lastSeenMessages.map((e) => ({ messageSender: e.sender, diff --git a/src/client/encrypt.js b/src/client/encrypt.js -index b9d21bab9faccd5dbf1975fc423fc55c73e906c5..99ffd76527b410e3a393181beb260108f4c63536 100644 +index 63cc2bd9615100bd2fd63dfe14c094aa6b8cd1c9..36df57d1196af9761d920fa285ac48f85410eaef 100644 --- a/src/client/encrypt.js +++ b/src/client/encrypt.js @@ -25,7 +25,11 @@ module.exports = function (client, options) { @@ -71,7 +71,7 @@ index b9d21bab9faccd5dbf1975fc423fc55c73e906c5..99ffd76527b410e3a393181beb260108 + // clearTimeout(loginTimeout) + // }) } - + function onJoinServerResponse (err) { diff --git a/src/client/play.js b/src/client/play.js index 559607f34e9a5b2b7809423f8ca4cd6746b60225..4dc1c3139438cc2729b05c57e57bd00252728f8a 100644 @@ -87,27 +87,10 @@ index 559607f34e9a5b2b7809423f8ca4cd6746b60225..4dc1c3139438cc2729b05c57e57bd002 }) // Server should send finish_configuration on its own right after sending the client a dimension codec diff --git a/src/client.js b/src/client.js -index 5c7a62b013daa69be91ec9e763b1f48ffe96ffa6..174d42a77740a937afcb106e1f39a9ee824a24b9 100644 +index e369e77d055ba919e8f9da7b8e8b5dc879c74cf4..11c6bff299f1186ab1ecb6744f53ff0c648ab192 100644 --- a/src/client.js +++ b/src/client.js -@@ -89,10 +89,12 @@ class Client extends EventEmitter { - parsed.metadata.name = parsed.data.name - parsed.data = parsed.data.params - parsed.metadata.state = state -- debug('read packet ' + state + '.' + parsed.metadata.name) -- if (debug.enabled) { -- const s = JSON.stringify(parsed.data, null, 2) -- debug(s && s.length > 10000 ? parsed.data : s) -+ if (!globalThis.excludeCommunicationDebugEvents?.includes(parsed.metadata.name)) { -+ debug('read packet ' + state + '.' + parsed.metadata.name) -+ if (debug.enabled) { -+ const s = JSON.stringify(parsed.data, null, 2) -+ debug(s && s.length > 10000 ? parsed.data : s) -+ } - } - if (this._hasBundlePacket && parsed.metadata.name === 'bundle_delimiter') { - if (this._mcBundle.length) { // End bundle -@@ -110,7 +112,13 @@ class Client extends EventEmitter { +@@ -111,7 +111,13 @@ class Client extends EventEmitter { this._hasBundlePacket = false } } else { @@ -122,9 +105,9 @@ index 5c7a62b013daa69be91ec9e763b1f48ffe96ffa6..174d42a77740a937afcb106e1f39a9ee } }) } -@@ -168,7 +176,10 @@ class Client extends EventEmitter { +@@ -169,7 +175,10 @@ class Client extends EventEmitter { } - + const onFatalError = (err) => { - this.emit('error', err) + // todo find out what is trying to write after client disconnect @@ -133,8 +116,8 @@ index 5c7a62b013daa69be91ec9e763b1f48ffe96ffa6..174d42a77740a937afcb106e1f39a9ee + } endSocket() } - -@@ -197,6 +208,8 @@ class Client extends EventEmitter { + +@@ -198,6 +207,8 @@ class Client extends EventEmitter { serializer -> framer -> socket -> splitter -> deserializer */ if (this.serializer) { this.serializer.end() @@ -143,17 +126,48 @@ index 5c7a62b013daa69be91ec9e763b1f48ffe96ffa6..174d42a77740a937afcb106e1f39a9ee } else { if (this.socket) this.socket.end() } -@@ -238,8 +251,11 @@ class Client extends EventEmitter { - - write (name, params) { - if (!this.serializer.writable) { return } -- debug('writing packet ' + this.state + '.' + name) -- debug(params) -+ if (!globalThis.excludeCommunicationDebugEvents?.includes(name)) { -+ debug(`[${this.state}] from ${this.isServer ? 'server' : 'client'}: ` + name) -+ debug(params) -+ } +@@ -243,6 +254,7 @@ class Client extends EventEmitter { + debug('writing packet ' + this.state + '.' + name) + debug(params) + } + this.emit('writePacket', name, params) this.serializer.write({ name, params }) } - + +diff --git a/src/client.js.rej b/src/client.js.rej +new file mode 100644 +index 0000000000000000000000000000000000000000..1101e2477adfdc004381b78e7d70953dacb7b484 +--- /dev/null ++++ b/src/client.js.rej +@@ -0,0 +1,31 @@ ++@@ -89,10 +89,12 @@ ++ parsed.metadata.name = parsed.data.name ++ parsed.data = parsed.data.params ++ parsed.metadata.state = state ++- debug('read packet ' + state + '.' + parsed.metadata.name) ++- if (debug.enabled) { ++- const s = JSON.stringify(parsed.data, null, 2) ++- debug(s && s.length > 10000 ? parsed.data : s) +++ if (!globalThis.excludeCommunicationDebugEvents?.includes(parsed.metadata.name)) { +++ debug('read packet ' + state + '.' + parsed.metadata.name) +++ if (debug.enabled) { +++ const s = JSON.stringify(parsed.data, null, 2) +++ debug(s && s.length > 10000 ? parsed.data : s) +++ } ++ } ++ if (this._hasBundlePacket && parsed.metadata.name === 'bundle_delimiter') { ++ if (this._mcBundle.length) { // End bundle ++@@ -239,8 +252,11 @@ ++ ++ write (name, params) { ++ if (!this.serializer.writable) { return } ++- debug('writing packet ' + this.state + '.' + name) ++- debug(params) +++ if (!globalThis.excludeCommunicationDebugEvents?.includes(name)) { +++ debug(`[${this.state}] from ${this.isServer ? 'server' : 'client'}: ` + name) +++ debug(params) +++ } +++ this.emit('writePacket', name, params) ++ this.serializer.write({ name, params }) ++ } ++ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 52c2ff30..aa67e479 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,7 +22,7 @@ overrides: patchedDependencies: minecraft-protocol: - hash: a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0 + hash: 40871f9192322be5e5fc0018a0a506a43f33973816b1b544743817609b606150 path: patches/minecraft-protocol.patch mineflayer-item-map-downloader@1.2.0: hash: a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad @@ -136,13 +136,13 @@ importers: version: 4.17.21 mcraft-fun-mineflayer: specifier: ^0.1.23 - version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/26c04c26f8eed2499a7c56eb4664649f3f54b767(encoding@0.1.13)) + version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/c9c77d6511e37c452ebe48790724da165d6ad448(encoding@0.1.13)) minecraft-data: specifier: 3.92.0 version: 3.92.0 minecraft-protocol: specifier: github:PrismarineJS/node-minecraft-protocol#master - version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0)(encoding@0.1.13) + version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/ca5af6307a817b655a76c82f56a4c6c78e272453(patch_hash=40871f9192322be5e5fc0018a0a506a43f33973816b1b544743817609b606150)(encoding@0.1.13) mineflayer-item-map-downloader: specifier: github:zardoy/mineflayer-item-map-downloader version: https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad)(encoding@0.1.13) @@ -341,7 +341,7 @@ importers: version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/89c33d396f3fde4804c71f4be3c203ade1833b41(@types/react@18.3.18)(react@18.3.1) mineflayer: specifier: github:zardoy/mineflayer#gen-the-master - version: https://codeload.github.com/zardoy/mineflayer/tar.gz/26c04c26f8eed2499a7c56eb4664649f3f54b767(encoding@0.1.13) + version: https://codeload.github.com/zardoy/mineflayer/tar.gz/c9c77d6511e37c452ebe48790724da165d6ad448(encoding@0.1.13) mineflayer-mouse: specifier: ^0.1.14 version: 0.1.14 @@ -6651,9 +6651,9 @@ packages: resolution: {tarball: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/89c33d396f3fde4804c71f4be3c203ade1833b41} version: 1.0.1 - minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176: - resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176} - version: 1.58.0 + minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/ca5af6307a817b655a76c82f56a4c6c78e272453: + resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/ca5af6307a817b655a76c82f56a4c6c78e272453} + version: 1.60.0 engines: {node: '>=22'} minecraft-wrap@1.6.0: @@ -6678,9 +6678,9 @@ packages: resolution: {integrity: sha512-GtW4hkijyZbSu5LKYYD89xZu+XY7OoP7IkrCnNEn6EdPm0+vr2THoJgFGKrlze9/81+T+P3E4qvJXNFiU/zeJg==} engines: {node: '>=22'} - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/26c04c26f8eed2499a7c56eb4664649f3f54b767: - resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/26c04c26f8eed2499a7c56eb4664649f3f54b767} - version: 4.30.0 + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/c9c77d6511e37c452ebe48790724da165d6ad448: + resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/c9c77d6511e37c452ebe48790724da165d6ad448} + version: 4.31.0 engines: {node: '>=22'} minimalistic-assert@1.0.1: @@ -7392,8 +7392,8 @@ packages: prismarine-entity@2.5.0: resolution: {integrity: sha512-nRPCawUwf9r3iKqi4I7mZRlir1Ix+DffWYdWq6p/KNnmiXve+xHE5zv8XCdhZlUmOshugHv5ONl9o6ORAkCNIA==} - prismarine-item@1.16.0: - resolution: {integrity: sha512-88Tz+/6HquYIsDuseae5G3IbqLeMews2L+ba2gX+p6K6soU9nuFhCfbwN56QuB7d/jZFcWrCYAPE5+UhwWh67w==} + prismarine-item@1.17.0: + resolution: {integrity: sha512-wN1OjP+f+Uvtjo3KzeCkVSy96CqZ8yG7cvuvlGwcYupQ6ct7LtNkubHp0AHuLMJ0vbbfAC0oZ2bWOgI1DYp8WA==} prismarine-nbt@2.7.0: resolution: {integrity: sha512-Du9OLQAcCj3y29YtewOJbbV4ARaSUEJiTguw0PPQbPBy83f+eCyDRkyBpnXTi/KPyEpgYCzsjGzElevLpFoYGQ==} @@ -8324,6 +8324,7 @@ packages: source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions sourcemap-codec@1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} @@ -9668,7 +9669,7 @@ snapshots: '@babel/core': 7.26.9 '@babel/helper-compilation-targets': 7.26.5 '@babel/helper-plugin-utils': 7.26.5 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 lodash.debounce: 4.0.8 resolve: 1.22.10 transitivePeerDependencies: @@ -11318,7 +11319,7 @@ snapshots: minecraft-data: 3.92.0 mineflayer: 4.30.0(encoding@0.1.13) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 - prismarine-item: 1.16.0 + prismarine-item: 1.17.0 prismarine-physics: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b vec3: 0.1.10 transitivePeerDependencies: @@ -12881,7 +12882,7 @@ snapshots: dependencies: '@typescript-eslint/types': 6.1.0 '@typescript-eslint/visitor-keys': 6.1.0 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.1 @@ -13077,13 +13078,13 @@ snapshots: flatmap: 0.0.3 long: 5.3.1 minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/ca5af6307a817b655a76c82f56a4c6c78e272453(patch_hash=40871f9192322be5e5fc0018a0a506a43f33973816b1b544743817609b606150)(encoding@0.1.13) 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/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0) prismarine-entity: 2.5.0 - prismarine-item: 1.16.0 + prismarine-item: 1.17.0 prismarine-nbt: 2.7.0 prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.92.0) prismarine-windows: 2.9.0 @@ -13113,13 +13114,13 @@ snapshots: flatmap: 0.0.3 long: 5.3.1 minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/ca5af6307a817b655a76c82f56a4c6c78e272453(patch_hash=40871f9192322be5e5fc0018a0a506a43f33973816b1b544743817609b606150)(encoding@0.1.13) 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/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0) prismarine-entity: 2.5.0 - prismarine-item: 1.16.0 + prismarine-item: 1.17.0 prismarine-nbt: 2.7.0 prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.92.0) prismarine-windows: 2.9.0 @@ -14502,7 +14503,7 @@ snapshots: detect-port@1.6.1: dependencies: address: 1.2.2 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -16109,7 +16110,7 @@ snapshots: https-proxy-agent@4.0.0: dependencies: agent-base: 5.1.1 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -16956,13 +16957,13 @@ snapshots: maxrects-packer: '@zardoy/maxrects-packer@2.7.4' zod: 3.24.2 - mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/26c04c26f8eed2499a7c56eb4664649f3f54b767(encoding@0.1.13)): + mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/c9c77d6511e37c452ebe48790724da165d6ad448(encoding@0.1.13)): dependencies: '@zardoy/flying-squid': 0.0.49(encoding@0.1.13) exit-hook: 2.2.1 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0)(encoding@0.1.13) - mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/26c04c26f8eed2499a7c56eb4664649f3f54b767(encoding@0.1.13) - prismarine-item: 1.16.0 + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/ca5af6307a817b655a76c82f56a4c6c78e272453(patch_hash=40871f9192322be5e5fc0018a0a506a43f33973816b1b544743817609b606150)(encoding@0.1.13) + mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/c9c77d6511e37c452ebe48790724da165d6ad448(encoding@0.1.13) + prismarine-item: 1.17.0 ws: 8.18.1 transitivePeerDependencies: - bufferutil @@ -17191,7 +17192,7 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.12 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 decode-named-character-reference: 1.1.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -17279,7 +17280,7 @@ snapshots: - '@types/react' - react - minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0)(encoding@0.1.13): + minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/ca5af6307a817b655a76c82f56a4c6c78e272453(patch_hash=40871f9192322be5e5fc0018a0a506a43f33973816b1b544743817609b606150)(encoding@0.1.13): dependencies: '@types/node-rsa': 1.1.4 '@types/readable-stream': 4.0.18 @@ -17341,7 +17342,7 @@ snapshots: dependencies: change-case: 5.4.4 debug: 4.4.1 - prismarine-item: 1.16.0 + prismarine-item: 1.17.0 prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c transitivePeerDependencies: - supports-color @@ -17351,7 +17352,7 @@ snapshots: minecraft-data: 3.92.0 prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-entity: 2.5.0 - prismarine-item: 1.16.0 + prismarine-item: 1.17.0 prismarine-nbt: 2.7.0 prismarine-physics: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b vec3: 0.1.10 @@ -17359,13 +17360,13 @@ snapshots: mineflayer@4.30.0(encoding@0.1.13): dependencies: minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/ca5af6307a817b655a76c82f56a4c6c78e272453(patch_hash=40871f9192322be5e5fc0018a0a506a43f33973816b1b544743817609b606150)(encoding@0.1.13) prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chat: 1.11.0 prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0) prismarine-entity: 2.5.0 - prismarine-item: 1.16.0 + prismarine-item: 1.17.0 prismarine-nbt: 2.7.0 prismarine-physics: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b prismarine-recipe: 1.3.1(prismarine-registry@1.11.0) @@ -17379,17 +17380,17 @@ snapshots: - encoding - supports-color - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/26c04c26f8eed2499a7c56eb4664649f3f54b767(encoding@0.1.13): + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/c9c77d6511e37c452ebe48790724da165d6ad448(encoding@0.1.13): dependencies: '@nxg-org/mineflayer-physics-util': 1.8.10 minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/ca5af6307a817b655a76c82f56a4c6c78e272453(patch_hash=40871f9192322be5e5fc0018a0a506a43f33973816b1b544743817609b606150)(encoding@0.1.13) prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chat: 1.11.0 prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0) prismarine-entity: 2.5.0 - prismarine-item: 1.16.0 + prismarine-item: 1.17.0 prismarine-nbt: 2.7.0 prismarine-physics: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b prismarine-recipe: 1.3.1(prismarine-registry@1.11.0) @@ -18181,7 +18182,7 @@ snapshots: minecraft-data: 3.92.0 prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) prismarine-chat: 1.11.0 - prismarine-item: 1.16.0 + prismarine-item: 1.17.0 prismarine-nbt: 2.7.0 prismarine-registry: 1.11.0 @@ -18207,11 +18208,11 @@ snapshots: prismarine-entity@2.5.0: dependencies: prismarine-chat: 1.11.0 - prismarine-item: 1.16.0 + prismarine-item: 1.17.0 prismarine-registry: 1.11.0 vec3: 0.1.10 - prismarine-item@1.16.0: + prismarine-item@1.17.0: dependencies: prismarine-nbt: 2.7.0 prismarine-registry: 1.11.0 @@ -18265,7 +18266,7 @@ snapshots: prismarine-windows@2.9.0: dependencies: - prismarine-item: 1.16.0 + prismarine-item: 1.17.0 prismarine-registry: 1.11.0 typed-emitter: 2.1.0 From d41527edc8db54c26ce1edcdee3664e44e324726 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sun, 3 Aug 2025 03:33:37 +0300 Subject: [PATCH 121/181] manually fix lockfile because of silly pnpm dep resolution --- pnpm-lock.yaml | 31 ++----------------------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa67e479..b79d619e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6674,10 +6674,6 @@ packages: mineflayer-pathfinder@2.4.5: resolution: {integrity: sha512-Jh3JnUgRLwhMh2Dugo4SPza68C41y+NPP5sdsgxRu35ydndo70i1JJGxauVWbXrpNwIxYNztUw78aFyb7icw8g==} - mineflayer@4.30.0: - resolution: {integrity: sha512-GtW4hkijyZbSu5LKYYD89xZu+XY7OoP7IkrCnNEn6EdPm0+vr2THoJgFGKrlze9/81+T+P3E4qvJXNFiU/zeJg==} - engines: {node: '>=22'} - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/c9c77d6511e37c452ebe48790724da165d6ad448: resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/c9c77d6511e37c452ebe48790724da165d6ad448} version: 4.31.0 @@ -11317,7 +11313,7 @@ snapshots: dependencies: '@nxg-org/mineflayer-util-plugin': 1.8.4 minecraft-data: 3.92.0 - mineflayer: 4.30.0(encoding@0.1.13) + mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/c9c77d6511e37c452ebe48790724da165d6ad448(encoding@0.1.13) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-item: 1.17.0 prismarine-physics: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b @@ -17332,7 +17328,7 @@ snapshots: mineflayer-item-map-downloader@https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad)(encoding@0.1.13): dependencies: - mineflayer: 4.30.0(encoding@0.1.13) + mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/c9c77d6511e37c452ebe48790724da165d6ad448(encoding@0.1.13) sharp: 0.30.7 transitivePeerDependencies: - encoding @@ -17357,29 +17353,6 @@ snapshots: prismarine-physics: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b vec3: 0.1.10 - mineflayer@4.30.0(encoding@0.1.13): - dependencies: - minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/ca5af6307a817b655a76c82f56a4c6c78e272453(patch_hash=40871f9192322be5e5fc0018a0a506a43f33973816b1b544743817609b606150)(encoding@0.1.13) - prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) - prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 - prismarine-chat: 1.11.0 - prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0) - prismarine-entity: 2.5.0 - prismarine-item: 1.17.0 - prismarine-nbt: 2.7.0 - prismarine-physics: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b - prismarine-recipe: 1.3.1(prismarine-registry@1.11.0) - prismarine-registry: 1.11.0 - prismarine-windows: 2.9.0 - prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c - protodef: 1.18.0 - typed-emitter: 1.4.0 - vec3: 0.1.10 - transitivePeerDependencies: - - encoding - - supports-color - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/c9c77d6511e37c452ebe48790724da165d6ad448(encoding@0.1.13): dependencies: '@nxg-org/mineflayer-physics-util': 1.8.10 From d7bd26b6b5f94e838d04c4cf19a8b9acc5966c5e Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Wed, 6 Aug 2025 01:47:09 +0300 Subject: [PATCH 122/181] up protocol patch --- patches/minecraft-protocol.patch | 13 -------- pnpm-lock.yaml | 51 ++++++++++++++++++++++++-------- 2 files changed, 39 insertions(+), 25 deletions(-) diff --git a/patches/minecraft-protocol.patch b/patches/minecraft-protocol.patch index 108085a1..e74f7e1d 100644 --- a/patches/minecraft-protocol.patch +++ b/patches/minecraft-protocol.patch @@ -73,19 +73,6 @@ index 63cc2bd9615100bd2fd63dfe14c094aa6b8cd1c9..36df57d1196af9761d920fa285ac48f8 } function onJoinServerResponse (err) { -diff --git a/src/client/play.js b/src/client/play.js -index 559607f34e9a5b2b7809423f8ca4cd6746b60225..4dc1c3139438cc2729b05c57e57bd00252728f8a 100644 ---- a/src/client/play.js -+++ b/src/client/play.js -@@ -53,7 +53,7 @@ module.exports = function (client, options) { - client.write('configuration_acknowledged', {}) - } - client.state = states.CONFIGURATION -- client.on('select_known_packs', () => { -+ client.once('select_known_packs', () => { - client.write('select_known_packs', { packs: [] }) - }) - // Server should send finish_configuration on its own right after sending the client a dimension codec diff --git a/src/client.js b/src/client.js index e369e77d055ba919e8f9da7b8e8b5dc879c74cf4..11c6bff299f1186ab1ecb6744f53ff0c648ab192 100644 --- a/src/client.js diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b79d619e..db095f5b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,7 +22,7 @@ overrides: patchedDependencies: minecraft-protocol: - hash: 40871f9192322be5e5fc0018a0a506a43f33973816b1b544743817609b606150 + hash: b417b3b7c5fd96e59abab5c1075b86b88bada2c980e4b54df13ca69b8f0091d9 path: patches/minecraft-protocol.patch mineflayer-item-map-downloader@1.2.0: hash: a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad @@ -142,7 +142,7 @@ importers: version: 3.92.0 minecraft-protocol: specifier: github:PrismarineJS/node-minecraft-protocol#master - version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/ca5af6307a817b655a76c82f56a4c6c78e272453(patch_hash=40871f9192322be5e5fc0018a0a506a43f33973816b1b544743817609b606150)(encoding@0.1.13) + version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=b417b3b7c5fd96e59abab5c1075b86b88bada2c980e4b54df13ca69b8f0091d9)(encoding@0.1.13) mineflayer-item-map-downloader: specifier: github:zardoy/mineflayer-item-map-downloader version: https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad)(encoding@0.1.13) @@ -6651,9 +6651,9 @@ packages: resolution: {tarball: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/89c33d396f3fde4804c71f4be3c203ade1833b41} version: 1.0.1 - minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/ca5af6307a817b655a76c82f56a4c6c78e272453: - resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/ca5af6307a817b655a76c82f56a4c6c78e272453} - version: 1.60.0 + minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb: + resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb} + version: 1.60.1 engines: {node: '>=22'} minecraft-wrap@1.6.0: @@ -6674,6 +6674,10 @@ packages: mineflayer-pathfinder@2.4.5: resolution: {integrity: sha512-Jh3JnUgRLwhMh2Dugo4SPza68C41y+NPP5sdsgxRu35ydndo70i1JJGxauVWbXrpNwIxYNztUw78aFyb7icw8g==} + mineflayer@4.31.0: + resolution: {integrity: sha512-oqiNa5uP4kXiPlj4+Jn+9QozPMsMy0U8/YP5d6+KSAeWthtuJHeQqcYgWG5lkC3LHMqHqtEu4MNdXt6GZjFNTQ==} + engines: {node: '>=22'} + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/c9c77d6511e37c452ebe48790724da165d6ad448: resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/c9c77d6511e37c452ebe48790724da165d6ad448} version: 4.31.0 @@ -11313,7 +11317,7 @@ snapshots: dependencies: '@nxg-org/mineflayer-util-plugin': 1.8.4 minecraft-data: 3.92.0 - mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/c9c77d6511e37c452ebe48790724da165d6ad448(encoding@0.1.13) + mineflayer: 4.31.0(encoding@0.1.13) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-item: 1.17.0 prismarine-physics: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b @@ -13074,7 +13078,7 @@ snapshots: flatmap: 0.0.3 long: 5.3.1 minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/ca5af6307a817b655a76c82f56a4c6c78e272453(patch_hash=40871f9192322be5e5fc0018a0a506a43f33973816b1b544743817609b606150)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=b417b3b7c5fd96e59abab5c1075b86b88bada2c980e4b54df13ca69b8f0091d9)(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 @@ -13110,7 +13114,7 @@ snapshots: flatmap: 0.0.3 long: 5.3.1 minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/ca5af6307a817b655a76c82f56a4c6c78e272453(patch_hash=40871f9192322be5e5fc0018a0a506a43f33973816b1b544743817609b606150)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=b417b3b7c5fd96e59abab5c1075b86b88bada2c980e4b54df13ca69b8f0091d9)(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 @@ -16957,7 +16961,7 @@ snapshots: dependencies: '@zardoy/flying-squid': 0.0.49(encoding@0.1.13) exit-hook: 2.2.1 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/ca5af6307a817b655a76c82f56a4c6c78e272453(patch_hash=40871f9192322be5e5fc0018a0a506a43f33973816b1b544743817609b606150)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=b417b3b7c5fd96e59abab5c1075b86b88bada2c980e4b54df13ca69b8f0091d9)(encoding@0.1.13) mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/c9c77d6511e37c452ebe48790724da165d6ad448(encoding@0.1.13) prismarine-item: 1.17.0 ws: 8.18.1 @@ -17276,7 +17280,7 @@ snapshots: - '@types/react' - react - minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/ca5af6307a817b655a76c82f56a4c6c78e272453(patch_hash=40871f9192322be5e5fc0018a0a506a43f33973816b1b544743817609b606150)(encoding@0.1.13): + minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=b417b3b7c5fd96e59abab5c1075b86b88bada2c980e4b54df13ca69b8f0091d9)(encoding@0.1.13): dependencies: '@types/node-rsa': 1.1.4 '@types/readable-stream': 4.0.18 @@ -17328,7 +17332,7 @@ snapshots: mineflayer-item-map-downloader@https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad)(encoding@0.1.13): dependencies: - mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/c9c77d6511e37c452ebe48790724da165d6ad448(encoding@0.1.13) + mineflayer: 4.31.0(encoding@0.1.13) sharp: 0.30.7 transitivePeerDependencies: - encoding @@ -17353,11 +17357,34 @@ snapshots: prismarine-physics: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b vec3: 0.1.10 + mineflayer@4.31.0(encoding@0.1.13): + dependencies: + minecraft-data: 3.92.0 + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=b417b3b7c5fd96e59abab5c1075b86b88bada2c980e4b54df13ca69b8f0091d9)(encoding@0.1.13) + prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) + prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 + prismarine-chat: 1.11.0 + prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0) + prismarine-entity: 2.5.0 + prismarine-item: 1.17.0 + prismarine-nbt: 2.7.0 + prismarine-physics: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b + prismarine-recipe: 1.3.1(prismarine-registry@1.11.0) + prismarine-registry: 1.11.0 + prismarine-windows: 2.9.0 + prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c + protodef: 1.18.0 + typed-emitter: 1.4.0 + vec3: 0.1.10 + transitivePeerDependencies: + - encoding + - supports-color + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/c9c77d6511e37c452ebe48790724da165d6ad448(encoding@0.1.13): dependencies: '@nxg-org/mineflayer-physics-util': 1.8.10 minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/ca5af6307a817b655a76c82f56a4c6c78e272453(patch_hash=40871f9192322be5e5fc0018a0a506a43f33973816b1b544743817609b606150)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=b417b3b7c5fd96e59abab5c1075b86b88bada2c980e4b54df13ca69b8f0091d9)(encoding@0.1.13) prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chat: 1.11.0 From 167b49da08bf66f2617188f6d905e5efee9da42f Mon Sep 17 00:00:00 2001 From: Vitaly Date: Fri, 8 Aug 2025 01:07:52 +0200 Subject: [PATCH 123/181] fix: fix cannot write after stream was destroyed message (#413) --- README.MD | 2 +- patches/minecraft-protocol.patch | 77 +++++++++----------------------- pnpm-lock.yaml | 16 +++---- src/index.ts | 8 +++- 4 files changed, 36 insertions(+), 67 deletions(-) diff --git a/README.MD b/README.MD index e9127a73..7978cee5 100644 --- a/README.MD +++ b/README.MD @@ -54,7 +54,7 @@ Howerver, it's known that these browsers have issues: ### Versions Support -Server versions 1.8 - 1.21.4 are supported. +Server versions 1.8 - 1.21.5 are supported. First class versions (most of the features are tested on these versions): - 1.19.4 diff --git a/patches/minecraft-protocol.patch b/patches/minecraft-protocol.patch index e74f7e1d..5dec44d7 100644 --- a/patches/minecraft-protocol.patch +++ b/patches/minecraft-protocol.patch @@ -1,8 +1,8 @@ diff --git a/src/client/chat.js b/src/client/chat.js -index a50f4b988ad9fb29d5eb9e1633b498615aa9cd28..b8b819eef0762a77d9db5c0fb06be648303628c7 100644 +index 0021870994fc59a82f0ac8aba0a65a8be43ef2f4..a53fceb843105ea2a1d88722b3fc7c3b43cb102a 100644 --- a/src/client/chat.js +++ b/src/client/chat.js -@@ -110,7 +110,7 @@ module.exports = function (client, options) { +@@ -116,7 +116,7 @@ module.exports = function (client, options) { for (const player of packet.data) { if (player.chatSession) { client._players[player.uuid] = { @@ -11,8 +11,8 @@ index a50f4b988ad9fb29d5eb9e1633b498615aa9cd28..b8b819eef0762a77d9db5c0fb06be648 publicKeyDER: player.chatSession.publicKey.keyBytes, sessionUuid: player.chatSession.uuid } -@@ -120,7 +120,7 @@ module.exports = function (client, options) { - +@@ -126,7 +126,7 @@ module.exports = function (client, options) { + if (player.crypto) { client._players[player.uuid] = { - publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }), @@ -20,7 +20,7 @@ index a50f4b988ad9fb29d5eb9e1633b498615aa9cd28..b8b819eef0762a77d9db5c0fb06be648 publicKeyDER: player.crypto.publicKey, signature: player.crypto.signature, displayName: player.displayName || player.name -@@ -190,7 +190,7 @@ module.exports = function (client, options) { +@@ -196,7 +196,7 @@ module.exports = function (client, options) { if (mcData.supportFeature('useChatSessions')) { const tsDelta = BigInt(Date.now()) - packet.timestamp const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0 @@ -29,16 +29,16 @@ index a50f4b988ad9fb29d5eb9e1633b498615aa9cd28..b8b819eef0762a77d9db5c0fb06be648 if (verified) client._signatureCache.push(packet.signature) client.emit('playerChat', { globalIndex: packet.globalIndex, -@@ -356,7 +356,7 @@ module.exports = function (client, options) { +@@ -362,7 +362,7 @@ module.exports = function (client, options) { } } - + - client._signedChat = (message, options = {}) => { + client._signedChat = async (message, options = {}) => { options.timestamp = options.timestamp || BigInt(Date.now()) options.salt = options.salt || 1n - -@@ -401,7 +401,7 @@ module.exports = function (client, options) { + +@@ -407,7 +407,7 @@ module.exports = function (client, options) { message, timestamp: options.timestamp, salt: options.salt, @@ -47,7 +47,7 @@ index a50f4b988ad9fb29d5eb9e1633b498615aa9cd28..b8b819eef0762a77d9db5c0fb06be648 offset: client._lastSeenMessages.pending, checksum: computeChatChecksum(client._lastSeenMessages), // 1.21.5+ acknowledged -@@ -416,7 +416,7 @@ module.exports = function (client, options) { +@@ -422,7 +422,7 @@ module.exports = function (client, options) { message, timestamp: options.timestamp, salt: options.salt, @@ -71,10 +71,10 @@ index 63cc2bd9615100bd2fd63dfe14c094aa6b8cd1c9..36df57d1196af9761d920fa285ac48f8 + // clearTimeout(loginTimeout) + // }) } - + function onJoinServerResponse (err) { diff --git a/src/client.js b/src/client.js -index e369e77d055ba919e8f9da7b8e8b5dc879c74cf4..11c6bff299f1186ab1ecb6744f53ff0c648ab192 100644 +index e369e77d055ba919e8f9da7b8e8b5dc879c74cf4..54bb9e6644388e9b6bd42b3012951875989cdf0c 100644 --- a/src/client.js +++ b/src/client.js @@ -111,7 +111,13 @@ class Client extends EventEmitter { @@ -94,7 +94,7 @@ index e369e77d055ba919e8f9da7b8e8b5dc879c74cf4..11c6bff299f1186ab1ecb6744f53ff0c } @@ -169,7 +175,10 @@ class Client extends EventEmitter { } - + const onFatalError = (err) => { - this.emit('error', err) + // todo find out what is trying to write after client disconnect @@ -103,58 +103,23 @@ index e369e77d055ba919e8f9da7b8e8b5dc879c74cf4..11c6bff299f1186ab1ecb6744f53ff0c + } endSocket() } - -@@ -198,6 +207,8 @@ class Client extends EventEmitter { + +@@ -198,6 +207,10 @@ class Client extends EventEmitter { serializer -> framer -> socket -> splitter -> deserializer */ if (this.serializer) { this.serializer.end() -+ this.socket?.end() -+ this.socket?.emit('end') ++ setTimeout(() => { ++ this.socket?.end() ++ this.socket?.emit('end') ++ }, 2000) // allow the serializer to finish writing } else { if (this.socket) this.socket.end() } -@@ -243,6 +254,7 @@ class Client extends EventEmitter { +@@ -243,6 +256,7 @@ class Client extends EventEmitter { debug('writing packet ' + this.state + '.' + name) debug(params) } + this.emit('writePacket', name, params) this.serializer.write({ name, params }) } - -diff --git a/src/client.js.rej b/src/client.js.rej -new file mode 100644 -index 0000000000000000000000000000000000000000..1101e2477adfdc004381b78e7d70953dacb7b484 ---- /dev/null -+++ b/src/client.js.rej -@@ -0,0 +1,31 @@ -+@@ -89,10 +89,12 @@ -+ parsed.metadata.name = parsed.data.name -+ parsed.data = parsed.data.params -+ parsed.metadata.state = state -+- debug('read packet ' + state + '.' + parsed.metadata.name) -+- if (debug.enabled) { -+- const s = JSON.stringify(parsed.data, null, 2) -+- debug(s && s.length > 10000 ? parsed.data : s) -++ if (!globalThis.excludeCommunicationDebugEvents?.includes(parsed.metadata.name)) { -++ debug('read packet ' + state + '.' + parsed.metadata.name) -++ if (debug.enabled) { -++ const s = JSON.stringify(parsed.data, null, 2) -++ debug(s && s.length > 10000 ? parsed.data : s) -++ } -+ } -+ if (this._hasBundlePacket && parsed.metadata.name === 'bundle_delimiter') { -+ if (this._mcBundle.length) { // End bundle -+@@ -239,8 +252,11 @@ -+ -+ write (name, params) { -+ if (!this.serializer.writable) { return } -+- debug('writing packet ' + this.state + '.' + name) -+- debug(params) -++ if (!globalThis.excludeCommunicationDebugEvents?.includes(name)) { -++ debug(`[${this.state}] from ${this.isServer ? 'server' : 'client'}: ` + name) -++ debug(params) -++ } -++ this.emit('writePacket', name, params) -+ this.serializer.write({ name, params }) -+ } -+ + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index db095f5b..132ef32c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,7 +22,7 @@ overrides: patchedDependencies: minecraft-protocol: - hash: b417b3b7c5fd96e59abab5c1075b86b88bada2c980e4b54df13ca69b8f0091d9 + hash: 2a88e61fea1825d9fa5f1584fde810421d553d23e45e6dc829c3697ee3358bea path: patches/minecraft-protocol.patch mineflayer-item-map-downloader@1.2.0: hash: a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad @@ -142,7 +142,7 @@ importers: version: 3.92.0 minecraft-protocol: specifier: github:PrismarineJS/node-minecraft-protocol#master - version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=b417b3b7c5fd96e59abab5c1075b86b88bada2c980e4b54df13ca69b8f0091d9)(encoding@0.1.13) + version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=2a88e61fea1825d9fa5f1584fde810421d553d23e45e6dc829c3697ee3358bea)(encoding@0.1.13) mineflayer-item-map-downloader: specifier: github:zardoy/mineflayer-item-map-downloader version: https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad)(encoding@0.1.13) @@ -13078,7 +13078,7 @@ snapshots: flatmap: 0.0.3 long: 5.3.1 minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=b417b3b7c5fd96e59abab5c1075b86b88bada2c980e4b54df13ca69b8f0091d9)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=2a88e61fea1825d9fa5f1584fde810421d553d23e45e6dc829c3697ee3358bea)(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 @@ -13114,7 +13114,7 @@ snapshots: flatmap: 0.0.3 long: 5.3.1 minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=b417b3b7c5fd96e59abab5c1075b86b88bada2c980e4b54df13ca69b8f0091d9)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=2a88e61fea1825d9fa5f1584fde810421d553d23e45e6dc829c3697ee3358bea)(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 @@ -16961,7 +16961,7 @@ snapshots: dependencies: '@zardoy/flying-squid': 0.0.49(encoding@0.1.13) exit-hook: 2.2.1 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=b417b3b7c5fd96e59abab5c1075b86b88bada2c980e4b54df13ca69b8f0091d9)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=2a88e61fea1825d9fa5f1584fde810421d553d23e45e6dc829c3697ee3358bea)(encoding@0.1.13) mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/c9c77d6511e37c452ebe48790724da165d6ad448(encoding@0.1.13) prismarine-item: 1.17.0 ws: 8.18.1 @@ -17280,7 +17280,7 @@ snapshots: - '@types/react' - react - minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=b417b3b7c5fd96e59abab5c1075b86b88bada2c980e4b54df13ca69b8f0091d9)(encoding@0.1.13): + minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=2a88e61fea1825d9fa5f1584fde810421d553d23e45e6dc829c3697ee3358bea)(encoding@0.1.13): dependencies: '@types/node-rsa': 1.1.4 '@types/readable-stream': 4.0.18 @@ -17360,7 +17360,7 @@ snapshots: mineflayer@4.31.0(encoding@0.1.13): dependencies: minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=b417b3b7c5fd96e59abab5c1075b86b88bada2c980e4b54df13ca69b8f0091d9)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=2a88e61fea1825d9fa5f1584fde810421d553d23e45e6dc829c3697ee3358bea)(encoding@0.1.13) prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chat: 1.11.0 @@ -17384,7 +17384,7 @@ snapshots: dependencies: '@nxg-org/mineflayer-physics-util': 1.8.10 minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=b417b3b7c5fd96e59abab5c1075b86b88bada2c980e4b54df13ca69b8f0091d9)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=2a88e61fea1825d9fa5f1584fde810421d553d23e45e6dc829c3697ee3358bea)(encoding@0.1.13) prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chat: 1.11.0 diff --git a/src/index.ts b/src/index.ts index d28261f4..c0213f56 100644 --- a/src/index.ts +++ b/src/index.ts @@ -231,8 +231,12 @@ export async function connect (connectOptions: ConnectOptions) { bot.emit('end', '') bot.removeAllListeners() bot._client.removeAllListeners() - //@ts-expect-error TODO? - bot._client = undefined + bot._client = { + //@ts-expect-error + write (packetName) { + console.warn('Tried to write packet', packetName, 'after bot was destroyed') + } + } //@ts-expect-error window.bot = bot = undefined } From caf4695637f1e1b86a98db90f12021df00822ca7 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Fri, 8 Aug 2025 18:33:20 +0300 Subject: [PATCH 124/181] feat: silly player on fire renderer effect --- renderer/viewer/lib/basePlayerState.ts | 1 + src/entities.ts | 63 ++++++++++ src/react/FireRenderer.tsx | 152 +++++++++++++++++++++++++ src/reactUi.tsx | 2 + 4 files changed, 218 insertions(+) create mode 100644 src/react/FireRenderer.tsx diff --git a/renderer/viewer/lib/basePlayerState.ts b/renderer/viewer/lib/basePlayerState.ts index af5d9d06..9cf1350a 100644 --- a/renderer/viewer/lib/basePlayerState.ts +++ b/renderer/viewer/lib/basePlayerState.ts @@ -48,6 +48,7 @@ export const getInitialPlayerState = () => proxy({ heldItemMain: undefined as HandItemBlock | undefined, heldItemOff: undefined as HandItemBlock | undefined, perspective: 'first_person' as CameraPerspective, + onFire: false, cameraSpectatingEntity: undefined as number | undefined, diff --git a/src/entities.ts b/src/entities.ts index 79602aa5..cf91ff2c 100644 --- a/src/entities.ts +++ b/src/entities.ts @@ -126,6 +126,28 @@ customEvents.on('gameLoaded', () => { if (entityStatus === EntityStatus.HURT) { getThreeJsRendererMethods()?.damageEntity(entityId, entityStatus) } + + if (entityStatus === EntityStatus.BURNED) { + updateEntityStates(entityId, true, true) + } + }) + + // on fire events + bot._client.on('entity_metadata', (data) => { + if (data.entityId !== bot.entity.id) return + handleEntityMetadata(data) + }) + + bot.on('end', () => { + if (onFireTimeout) { + clearTimeout(onFireTimeout) + } + }) + + bot.on('respawn', () => { + if (onFireTimeout) { + clearTimeout(onFireTimeout) + } }) const updateCamera = (entity: Entity) => { @@ -296,3 +318,44 @@ customEvents.on('gameLoaded', () => { }) }) + +// Constants +const SHARED_FLAGS_KEY = 0 +const ENTITY_FLAGS = { + ON_FIRE: 0x01, // Bit 0 + SNEAKING: 0x02, // Bit 1 + SPRINTING: 0x08, // Bit 3 + SWIMMING: 0x10, // Bit 4 + INVISIBLE: 0x20, // Bit 5 + GLOWING: 0x40, // Bit 6 + FALL_FLYING: 0x80 // Bit 7 (elytra flying) +} + +let onFireTimeout: NodeJS.Timeout | undefined +const updateEntityStates = (entityId: number, onFire: boolean, timeout?: boolean) => { + if (entityId !== bot.entity.id) return + appViewer.playerState.reactive.onFire = onFire + if (onFireTimeout) { + clearTimeout(onFireTimeout) + } + if (timeout) { + onFireTimeout = setTimeout(() => { + updateEntityStates(entityId, false, false) + }, 5000) + } +} + +// Process entity metadata packet +function handleEntityMetadata (packet: { entityId: number, metadata: Array<{ key: number, type: string, value: number }> }) { + const { entityId, metadata } = packet + + // Find shared flags in metadata + const flagsData = metadata.find(meta => meta.key === SHARED_FLAGS_KEY && + meta.type === 'byte') + + // Update fire state if flags were found + if (flagsData) { + const wasOnFire = appViewer.playerState.reactive.onFire + appViewer.playerState.reactive.onFire = (flagsData.value & ENTITY_FLAGS.ON_FIRE) !== 0 + } +} diff --git a/src/react/FireRenderer.tsx b/src/react/FireRenderer.tsx new file mode 100644 index 00000000..3a188558 --- /dev/null +++ b/src/react/FireRenderer.tsx @@ -0,0 +1,152 @@ +/* eslint-disable no-await-in-loop */ +import { useSnapshot } from 'valtio' +import { useEffect, useState } from 'react' +import { getLoadedImage } from 'mc-assets/dist/utils' +import { createCanvas } from 'renderer/viewer/lib/utils' + +const TEXTURE_UPDATE_INTERVAL = 100 // 5 times per second + +export default () => { + const { onFire, perspective } = useSnapshot(appViewer.playerState.reactive) + const [fireTextures, setFireTextures] = useState([]) + const [currentTextureIndex, setCurrentTextureIndex] = useState(0) + + useEffect(() => { + let animationFrameId: number + let lastTextureUpdate = 0 + + const updateTexture = (timestamp: number) => { + if (onFire && fireTextures.length > 0) { + if (timestamp - lastTextureUpdate >= TEXTURE_UPDATE_INTERVAL) { + setCurrentTextureIndex(prev => (prev + 1) % fireTextures.length) + lastTextureUpdate = timestamp + } + } + animationFrameId = requestAnimationFrame(updateTexture) + } + + animationFrameId = requestAnimationFrame(updateTexture) + return () => cancelAnimationFrame(animationFrameId) + }, [onFire, fireTextures]) + + useEffect(() => { + const loadTextures = async () => { + const fireImageUrls: string[] = [] + + const { resourcesManager } = appViewer + const { blocksAtlasParser } = resourcesManager + if (!blocksAtlasParser?.atlas?.latest) { + console.warn('FireRenderer: Blocks atlas parser not available') + return + } + + const keys = Object.keys(blocksAtlasParser.atlas.latest.textures).filter(key => /^fire_\d+$/.exec(key)) + for (const key of keys) { + const textureInfo = blocksAtlasParser.getTextureInfo(key) as { u: number, v: number, width?: number, height?: number } + if (textureInfo) { + const defaultSize = blocksAtlasParser.atlas.latest.tileSize + const imageWidth = blocksAtlasParser.atlas.latest.width + const imageHeight = blocksAtlasParser.atlas.latest.height + const textureWidth = textureInfo.width ?? defaultSize + const textureHeight = textureInfo.height ?? defaultSize + + // Create a temporary canvas for the full texture + const tempCanvas = createCanvas(textureWidth, textureHeight) + const tempCtx = tempCanvas.getContext('2d') + if (tempCtx && blocksAtlasParser.latestImage) { + const image = await getLoadedImage(blocksAtlasParser.latestImage) + tempCtx.drawImage( + image, + textureInfo.u * imageWidth, + textureInfo.v * imageHeight, + textureWidth, + textureHeight, + 0, + 0, + textureWidth, + textureHeight + ) + + // Create final canvas with only top 20% of the texture + const finalHeight = Math.ceil(textureHeight * 0.4) + const canvas = createCanvas(textureWidth, finalHeight) + const ctx = canvas.getContext('2d') + if (ctx) { + // Draw only the top portion + ctx.drawImage( + tempCanvas, + 0, + 0, // Start from top + textureWidth, + finalHeight, + 0, + 0, + textureWidth, + finalHeight + ) + + const blob = await canvas.convertToBlob() + const url = URL.createObjectURL(blob) + fireImageUrls.push(url) + } + } + } + } + + setFireTextures(fireImageUrls) + } + + // Load textures initially + if (appViewer.resourcesManager.currentResources) { + void loadTextures() + } + + // Set up listener for texture updates + const onAssetsUpdated = () => { + void loadTextures() + } + appViewer.resourcesManager.on('assetsTexturesUpdated', onAssetsUpdated) + + // Cleanup + return () => { + appViewer.resourcesManager.off('assetsTexturesUpdated', onAssetsUpdated) + // Cleanup texture URLs + for (const url of fireTextures) URL.revokeObjectURL(url) + } + }, []) + + if (!onFire || fireTextures.length === 0 || perspective !== 'first_person') return null + + return ( +
    +
    +
    + ) +} diff --git a/src/reactUi.tsx b/src/reactUi.tsx index b15cb79d..4f8c4541 100644 --- a/src/reactUi.tsx +++ b/src/reactUi.tsx @@ -66,6 +66,7 @@ import CreditsAboutModal from './react/CreditsAboutModal' import GlobalOverlayHints from './react/GlobalOverlayHints' import FullscreenTime from './react/FullscreenTime' import StorageConflictModal from './react/StorageConflictModal' +import FireRenderer from './react/FireRenderer' const isFirefox = ua.getBrowser().name === 'Firefox' if (isFirefox) { @@ -171,6 +172,7 @@ const InGameUi = () => { + {!disabledUiParts.includes('fire') && }
    From 53cbff7699d5255974a09e5f1e50b02759116a63 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Fri, 8 Aug 2025 18:37:10 +0300 Subject: [PATCH 125/181] dont conflict fire with chat --- src/react/FireRenderer.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/react/FireRenderer.tsx b/src/react/FireRenderer.tsx index 3a188558..20ad4606 100644 --- a/src/react/FireRenderer.tsx +++ b/src/react/FireRenderer.tsx @@ -130,7 +130,8 @@ export default () => { display: 'flex', justifyContent: 'center', alignItems: 'flex-end', - overflow: 'hidden' + overflow: 'hidden', + zIndex: -1 }} >
    Date: Fri, 8 Aug 2025 21:52:55 +0300 Subject: [PATCH 126/181] fix: some blocks textures were not update in hotbar after texturepack change --- renderer/viewer/three/renderSlot.ts | 20 +++++++++++++------- src/inventoryWindows.ts | 1 + src/react/HotbarRenderApp.tsx | 3 ++- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/renderer/viewer/three/renderSlot.ts b/renderer/viewer/three/renderSlot.ts index d82e58e3..321633eb 100644 --- a/renderer/viewer/three/renderSlot.ts +++ b/renderer/viewer/three/renderSlot.ts @@ -10,11 +10,11 @@ export type ResolvedItemModelRender = { export const renderSlot = (model: ResolvedItemModelRender, resourcesManager: ResourcesManagerCommon, debugIsQuickbar = false, fullBlockModelSupport = false): { texture: string, - blockData?: Record & { resolvedModel: BlockModel }, - scale?: number, - slice?: number[], - modelName?: string, -} | undefined => { + blockData: Record & { resolvedModel: BlockModel } | null, + scale: number | null, + slice: number[] | null, + modelName: string | null, +} => { let itemModelName = model.modelName const isItem = loadedData.itemsByName[itemModelName] @@ -37,6 +37,8 @@ export const renderSlot = (model: ResolvedItemModelRender, resourcesManager: Res texture: 'gui', slice: [x, y, atlas.tileSize, atlas.tileSize], scale: 0.25, + blockData: null, + modelName: null } } } @@ -63,14 +65,18 @@ export const renderSlot = (model: ResolvedItemModelRender, resourcesManager: Res return { texture: itemTexture.type, slice: itemTexture.slice, - modelName: itemModelName + modelName: itemModelName, + blockData: null, + scale: null } } else { // is block return { texture: 'blocks', blockData: itemTexture, - modelName: itemModelName + modelName: itemModelName, + slice: null, + scale: null } } } diff --git a/src/inventoryWindows.ts b/src/inventoryWindows.ts index bc7dcbaf..a9f89d1b 100644 --- a/src/inventoryWindows.ts +++ b/src/inventoryWindows.ts @@ -259,6 +259,7 @@ export const upInventoryItems = (isInventory: boolean, invWindow = lastWindow) = // inv.pwindow.inv.slots[2].blockData = getBlockData('dirt') const customSlots = mapSlots((isInventory ? bot.inventory : bot.currentWindow)!.slots) invWindow.pwindow.setSlots(customSlots) + return customSlots } export const onModalClose = (callback: () => any) => { diff --git a/src/react/HotbarRenderApp.tsx b/src/react/HotbarRenderApp.tsx index 6b6e3207..c782e6ef 100644 --- a/src/react/HotbarRenderApp.tsx +++ b/src/react/HotbarRenderApp.tsx @@ -115,7 +115,7 @@ const HotbarInner = () => { container.current.appendChild(inv.canvas) const upHotbarItems = () => { if (!appViewer.resourcesManager?.itemsAtlasParser) return - upInventoryItems(true, inv) + globalThis.debugHotbarItems = upInventoryItems(true, inv) } canvasManager.canvas.onclick = (e) => { @@ -127,6 +127,7 @@ const HotbarInner = () => { } } + globalThis.debugUpHotbarItems = upHotbarItems upHotbarItems() bot.inventory.on('updateSlot', upHotbarItems) appViewer.resourcesManager.on('assetsTexturesUpdated', upHotbarItems) From fb395041b9e40449819e5a8d0361f06264bfa80b Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Mon, 11 Aug 2025 01:39:08 +0300 Subject: [PATCH 127/181] fix: fix on 1.18.2 many blocks like mushrom blocks, fence gates, deepslate, basalt, copper stuff like ore, infested stone, cakes and tinted glass was resulting in instant breaking on the client dev: add debugTestPing --- scripts/makeOptimizedMcData.mjs | 7 +++-- src/mineflayer/mc-protocol.ts | 49 ++++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/scripts/makeOptimizedMcData.mjs b/scripts/makeOptimizedMcData.mjs index 0b5752d8..76e0f1c2 100644 --- a/scripts/makeOptimizedMcData.mjs +++ b/scripts/makeOptimizedMcData.mjs @@ -90,16 +90,19 @@ const dataTypeBundling = { }, blocks: { arrKey: 'name', - processData(current, prev) { + processData(current, prev, _, version) { for (const block of current) { + const prevBlock = prev?.find(x => x.name === block.name) if (block.transparent) { const forceOpaque = block.name.includes('shulker_box') || block.name.match(/^double_.+_slab\d?$/) || ['melon_block', 'lit_pumpkin', 'lit_redstone_ore', 'lit_furnace'].includes(block.name) - const prevBlock = prev?.find(x => x.name === block.name) if (forceOpaque || (prevBlock && !prevBlock.transparent)) { block.transparent = false } } + if (block.hardness === 0 && prevBlock && prevBlock.hardness > 0) { + block.hardness = prevBlock.hardness + } } } // ignoreRemoved: true, diff --git a/src/mineflayer/mc-protocol.ts b/src/mineflayer/mc-protocol.ts index 2376cd03..a0348c5d 100644 --- a/src/mineflayer/mc-protocol.ts +++ b/src/mineflayer/mc-protocol.ts @@ -1,8 +1,11 @@ +import net from 'net' import { Client } from 'minecraft-protocol' import { appQueryParams } from '../appParams' import { downloadAllMinecraftData, getVersionAutoSelect } from '../connect' import { gameAdditionalState } from '../globalState' import { ProgressReporter } from '../core/progressReporter' +import { parseServerAddress } from '../parseServerAddress' +import { getCurrentProxy } from '../react/ServersList' import { pingServerVersion, validatePacket } from './minecraft-protocol-extra' import { getWebsocketStream } from './websocket-core' @@ -35,7 +38,7 @@ setInterval(() => { }, 1000) -export const getServerInfo = async (ip: string, port?: number, preferredVersion = getVersionAutoSelect(), ping = false, progressReporter?: ProgressReporter) => { +export const getServerInfo = async (ip: string, port?: number, preferredVersion = getVersionAutoSelect(), ping = false, progressReporter?: ProgressReporter, setProxyParams?: ProxyParams) => { await downloadAllMinecraftData() const isWebSocket = ip.startsWith('ws://') || ip.startsWith('wss://') let stream @@ -43,6 +46,8 @@ export const getServerInfo = async (ip: string, port?: number, preferredVersion progressReporter?.setMessage('Connecting to WebSocket server') stream = (await getWebsocketStream(ip)).mineflayerStream progressReporter?.setMessage('WebSocket connected. Ping packet sent, waiting for response') + } else if (setProxyParams) { + setProxy(setProxyParams) } window.setLoadingMessage = (message?: string) => { if (message === undefined) { @@ -59,3 +64,45 @@ export const getServerInfo = async (ip: string, port?: number, preferredVersion window.setLoadingMessage = undefined }) } + +globalThis.debugTestPing = async (ip: string) => { + const parsed = parseServerAddress(ip, false) + const result = await getServerInfo(parsed.host, parsed.port ? Number(parsed.port) : undefined, undefined, true, undefined, { address: getCurrentProxy(), }) + console.log('result', result) + return result +} + +export const getDefaultProxyParams = () => { + return { + headers: { + Authorization: `Bearer ${new URLSearchParams(location.search).get('token') ?? ''}` + } + } +} + +export type ProxyParams = { + address?: string + headers?: Record +} + +export const setProxy = (proxyParams: ProxyParams) => { + if (proxyParams.address?.startsWith(':')) { + proxyParams.address = `${location.protocol}//${location.hostname}${proxyParams.address}` + } + if (proxyParams.address && location.port !== '80' && location.port !== '443' && !/:\d+$/.test(proxyParams.address)) { + const https = proxyParams.address.startsWith('https://') || location.protocol === 'https:' + proxyParams.address = `${proxyParams.address}:${https ? 443 : 80}` + } + + const parsedProxy = parseServerAddress(proxyParams.address, false) + const proxy = { host: parsedProxy.host, port: parsedProxy.port } + proxyParams.headers ??= getDefaultProxyParams().headers + net['setProxy']({ + hostname: proxy.host, + port: proxy.port, + headers: proxyParams.headers + }) + return { + proxy + } +} From e7c358d3fc0497b66fa25c70a401d2aed4be3294 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Mon, 11 Aug 2025 03:12:05 +0300 Subject: [PATCH 128/181] feat: add `minecraft-web-client:block-interactions-customization` --- package.json | 2 +- pnpm-lock.yaml | 20 ++++++++++---------- src/customChannels.ts | 31 +++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 257fff7c..b719b877 100644 --- a/package.json +++ b/package.json @@ -156,7 +156,7 @@ "mc-assets": "^0.2.62", "minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next", "mineflayer": "github:zardoy/mineflayer#gen-the-master", - "mineflayer-mouse": "^0.1.14", + "mineflayer-mouse": "^0.1.17", "mineflayer-pathfinder": "^2.4.4", "npm-run-all": "^4.1.5", "os-browserify": "^0.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 132ef32c..da2b3912 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -343,8 +343,8 @@ importers: specifier: github:zardoy/mineflayer#gen-the-master version: https://codeload.github.com/zardoy/mineflayer/tar.gz/c9c77d6511e37c452ebe48790724da165d6ad448(encoding@0.1.13) mineflayer-mouse: - specifier: ^0.1.14 - version: 0.1.14 + specifier: ^0.1.17 + version: 0.1.17 mineflayer-pathfinder: specifier: ^2.4.4 version: 2.4.5 @@ -6667,8 +6667,8 @@ packages: resolution: {tarball: https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824} version: 1.2.0 - mineflayer-mouse@0.1.14: - resolution: {integrity: sha512-DjytRMlRLxR44GqZ6udMgbMO4At7Ura5TQC80exRhzkfptyCGLTWzXaf0oeXSNYkNMnaaEv4XP/9YRwuvL+rsQ==} + mineflayer-mouse@0.1.17: + resolution: {integrity: sha512-0eCR8pnGb42Qd9QmAxOjl0PhA5Fa+9+6H1G/YsbsO5rg5mDf94Tusqp/8NAGLPQCPVDzbarLskXdjR3h0E0bEQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} mineflayer-pathfinder@2.4.5: @@ -10294,7 +10294,7 @@ snapshots: '@babel/parser': 7.26.9 '@babel/template': 7.26.9 '@babel/types': 7.26.9 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -12864,7 +12864,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 6.1.0(typescript@5.5.4) '@typescript-eslint/utils': 6.1.0(eslint@8.57.1)(typescript@5.5.4) - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 eslint: 8.57.1 ts-api-utils: 1.4.3(typescript@5.5.4) optionalDependencies: @@ -12896,7 +12896,7 @@ snapshots: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -12911,7 +12911,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.26.0 '@typescript-eslint/visitor-keys': 8.26.0 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -17338,7 +17338,7 @@ snapshots: - encoding - supports-color - mineflayer-mouse@0.1.14: + mineflayer-mouse@0.1.17: dependencies: change-case: 5.4.4 debug: 4.4.1 @@ -18457,7 +18457,7 @@ snapshots: puppeteer-core@2.1.1: dependencies: '@types/mime-types': 2.1.4 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 extract-zip: 1.7.0 https-proxy-agent: 4.0.0 mime: 2.6.0 diff --git a/src/customChannels.ts b/src/customChannels.ts index 57c057d5..3f6a8217 100644 --- a/src/customChannels.ts +++ b/src/customChannels.ts @@ -15,6 +15,7 @@ export default () => { registerMediaChannels() registerSectionAnimationChannels() registeredJeiChannel() + registerBlockInteractionsCustomizationChannel() }) } @@ -32,6 +33,36 @@ const registerChannel = (channelName: string, packetStructure: any[], handler: ( console.debug(`registered custom channel ${channelName} channel`) } +const registerBlockInteractionsCustomizationChannel = () => { + const CHANNEL_NAME = 'minecraft-web-client:block-interactions-customization' + const packetStructure = [ + 'container', + [ + { + name: 'newConfiguration', + type: ['pstring', { countType: 'i16' }] + }, + ] + ] + + registerChannel(CHANNEL_NAME, packetStructure, (data) => { + const config = JSON.parse(data.newConfiguration) + if (config.customBreakTime !== undefined && Object.values(config.customBreakTime).every(x => typeof x === 'number')) { + bot.mouse.customBreakTime = config.customBreakTime + } + if (config.customBreakTimeToolAllowance !== undefined) { + bot.mouse.customBreakTimeToolAllowance = new Set(config.customBreakTimeToolAllowance) + } + + if (config.blockPlacePrediction !== undefined) { + bot.mouse.settings.blockPlacePrediction = config.blockPlacePrediction + } + if (config.blockPlacePredictionDelay !== undefined) { + bot.mouse.settings.blockPlacePredictionDelay = config.blockPlacePredictionDelay + } + }, true) +} + const registerBlockModelsChannel = () => { const CHANNEL_NAME = 'minecraft-web-client:blockmodels' From cdd8c31a0e9261ee57fb66ff8ca5af0e074bff78 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Mon, 11 Aug 2025 21:21:44 +0300 Subject: [PATCH 129/181] fix: fix player colored username rendering, fix sometimes skin was overriden --- renderer/viewer/three/entities.ts | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/renderer/viewer/three/entities.ts b/renderer/viewer/three/entities.ts index 6c6f8900..24f64803 100644 --- a/renderer/viewer/three/entities.ts +++ b/renderer/viewer/three/entities.ts @@ -141,7 +141,7 @@ const addNametag = (entity, options: { fontFamily: string }, mesh, version: stri const canvas = getUsernameTexture(entity, options, version) const tex = new THREE.Texture(canvas) tex.needsUpdate = true - let nameTag + let nameTag: THREE.Object3D if (entity.nameTagFixed) { const geometry = new THREE.PlaneGeometry() const material = new THREE.MeshBasicMaterial({ map: tex }) @@ -171,6 +171,7 @@ const addNametag = (entity, options: { fontFamily: string }, mesh, version: stri nameTag.name = 'nametag' mesh.add(nameTag) + return nameTag } } @@ -494,6 +495,10 @@ export class Entities { // todo true/undefined doesnt reset the skin to the default one // eslint-disable-next-line max-params async updatePlayerSkin (entityId: string | number, username: string | undefined, uuidCache: string | undefined, skinUrl: string | true, capeUrl: string | true | undefined = undefined) { + const isCustomSkin = skinUrl !== stevePngUrl + if (isCustomSkin) { + this.loadedSkinEntityIds.add(String(entityId)) + } if (uuidCache) { if (typeof skinUrl === 'string' || typeof capeUrl === 'string') this.uuidPerSkinUrlsCache[uuidCache] = {} if (typeof skinUrl === 'string') this.uuidPerSkinUrlsCache[uuidCache].skinUrl = skinUrl @@ -912,20 +917,14 @@ export class Entities { mesh = wrapper if (entity.username) { - // todo proper colors - const nameTag = new NameTagObject(fromFormattedString(entity.username).text, { - font: `48px ${this.entitiesOptions.fontFamily}`, - }) - nameTag.position.y = playerObject.position.y + playerObject.scale.y * 16 + 3 - nameTag.renderOrder = 1000 - - nameTag.name = 'nametag' - - //@ts-expect-error - wrapper.add(nameTag) + const nametag = addNametag(entity, { fontFamily: 'mojangles' }, wrapper, this.worldRenderer.version) + if (nametag) { + nametag.position.y = playerObject.position.y + playerObject.scale.y * 16 + 3 + nametag.scale.multiplyScalar(12) + } } } else { - mesh = getEntityMesh(entity, this.worldRenderer, this.entitiesOptions, overrides) + mesh = getEntityMesh(entity, this.worldRenderer, this.entitiesOptions, { ...overrides, customModel: entity['customModel'] }) } if (!mesh) return mesh.name = 'mesh' @@ -1181,8 +1180,7 @@ export class Entities { const cameraPos = this.worldRenderer.cameraObject.position const distance = mesh.position.distanceTo(cameraPos) if (distance < MAX_DISTANCE_SKIN_LOAD && distance < (this.worldRenderer.viewDistance * 16)) { - if (this.loadedSkinEntityIds.has(entityId)) return - this.loadedSkinEntityIds.add(entityId) + if (this.loadedSkinEntityIds.has(String(entityId))) return void this.updatePlayerSkin(entityId, mesh.playerObject.realUsername, mesh.playerObject.realPlayerUuid, true, true) } } From 0a474e67803827cdd657b135ba6a7380091cdab4 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Tue, 12 Aug 2025 06:27:06 +0300 Subject: [PATCH 130/181] feat: add custom experimental waypints impl --- experiments/three-labels.html | 5 + experiments/three-labels.ts | 67 ++++ renderer/viewer/three/graphicsBackend.ts | 3 + renderer/viewer/three/waypointSprite.ts | 394 ++++++++++++++++++++ renderer/viewer/three/waypoints.ts | 142 +++++++ renderer/viewer/three/worldrendererThree.ts | 25 +- src/customChannels.ts | 57 +++ 7 files changed, 685 insertions(+), 8 deletions(-) create mode 100644 experiments/three-labels.html create mode 100644 experiments/three-labels.ts create mode 100644 renderer/viewer/three/waypointSprite.ts create mode 100644 renderer/viewer/three/waypoints.ts diff --git a/experiments/three-labels.html b/experiments/three-labels.html new file mode 100644 index 00000000..2b25bc23 --- /dev/null +++ b/experiments/three-labels.html @@ -0,0 +1,5 @@ + + diff --git a/experiments/three-labels.ts b/experiments/three-labels.ts new file mode 100644 index 00000000..b69dc95b --- /dev/null +++ b/experiments/three-labels.ts @@ -0,0 +1,67 @@ +import * as THREE from 'three' +import { FirstPersonControls } from 'three/addons/controls/FirstPersonControls.js' +import { createWaypointSprite, WAYPOINT_CONFIG } from '../renderer/viewer/three/waypointSprite' + +// Create scene, camera and renderer +const scene = new THREE.Scene() +const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000) +const renderer = new THREE.WebGLRenderer({ antialias: true }) +renderer.setSize(window.innerWidth, window.innerHeight) +document.body.appendChild(renderer.domElement) + +// Add FirstPersonControls +const controls = new FirstPersonControls(camera, renderer.domElement) +controls.lookSpeed = 0.1 +controls.movementSpeed = 10 +controls.lookVertical = true +controls.constrainVertical = true +controls.verticalMin = 0.1 +controls.verticalMax = Math.PI - 0.1 + +// Position camera +camera.position.y = 1.6 // Typical eye height +camera.lookAt(0, 1.6, -1) + +// Create a helper grid and axes +const grid = new THREE.GridHelper(20, 20) +scene.add(grid) +const axes = new THREE.AxesHelper(5) +scene.add(axes) + +// Create waypoint sprite via utility +const waypoint = createWaypointSprite({ + position: new THREE.Vector3(0, 0, -5), + color: 0xff0000, + label: 'Target', +}) +scene.add(waypoint.group) + +// Use built-in offscreen arrow from utils +waypoint.enableOffscreenArrow(true) +waypoint.setArrowParent(scene) + +// Animation loop +function animate() { + requestAnimationFrame(animate) + + const delta = Math.min(clock.getDelta(), 0.1) + controls.update(delta) + + // Unified camera update (size, distance text, arrow, visibility) + const sizeVec = renderer.getSize(new THREE.Vector2()) + waypoint.updateForCamera(camera.position, camera, sizeVec.width, sizeVec.height) + + renderer.render(scene, camera) +} + +// Handle window resize +window.addEventListener('resize', () => { + camera.aspect = window.innerWidth / window.innerHeight + camera.updateProjectionMatrix() + renderer.setSize(window.innerWidth, window.innerHeight) +}) + +// Add clock for controls +const clock = new THREE.Clock() + +animate() diff --git a/renderer/viewer/three/graphicsBackend.ts b/renderer/viewer/three/graphicsBackend.ts index 5ea89b34..92b6ec66 100644 --- a/renderer/viewer/three/graphicsBackend.ts +++ b/renderer/viewer/three/graphicsBackend.ts @@ -44,6 +44,9 @@ const getBackendMethods = (worldRenderer: WorldRendererThree) => { shakeFromDamage: worldRenderer.cameraShake.shakeFromDamage.bind(worldRenderer.cameraShake), onPageInteraction: worldRenderer.media.onPageInteraction.bind(worldRenderer.media), downloadMesherLog: worldRenderer.downloadMesherLog.bind(worldRenderer), + + addWaypoint: worldRenderer.waypoints.addWaypoint.bind(worldRenderer.waypoints), + removeWaypoint: worldRenderer.waypoints.removeWaypoint.bind(worldRenderer.waypoints), } } diff --git a/renderer/viewer/three/waypointSprite.ts b/renderer/viewer/three/waypointSprite.ts new file mode 100644 index 00000000..7c8cf1f6 --- /dev/null +++ b/renderer/viewer/three/waypointSprite.ts @@ -0,0 +1,394 @@ +import * as THREE from 'three' + +// Centralized visual configuration (in screen pixels) +export const WAYPOINT_CONFIG = { + // Target size in screen pixels (this controls the final sprite size) + TARGET_SCREEN_PX: 150, + // Canvas size for internal rendering (keep power of 2 for textures) + CANVAS_SIZE: 256, + // Relative positions in canvas (0-1) + LAYOUT: { + DOT_Y: 0.3, + NAME_Y: 0.45, + DISTANCE_Y: 0.55, + }, + // Multiplier for canvas internal resolution to keep text crisp + CANVAS_SCALE: 2, + ARROW: { + enabledDefault: false, + pixelSize: 30, + paddingPx: 50, + }, +} + +export type WaypointSprite = { + group: THREE.Group + sprite: THREE.Sprite + // Offscreen arrow controls + enableOffscreenArrow: (enabled: boolean) => void + setArrowParent: (parent: THREE.Object3D | null) => void + // Convenience combined updater + updateForCamera: ( + cameraPosition: THREE.Vector3, + camera: THREE.PerspectiveCamera, + viewportWidthPx: number, + viewportHeightPx: number + ) => boolean + // Utilities + setColor: (color: number) => void + setLabel: (label?: string) => void + updateDistanceText: (label: string, distanceText: string) => void + setVisible: (visible: boolean) => void + setPosition: (x: number, y: number, z: number) => void + dispose: () => void +} + +export function createWaypointSprite (options: { + position: THREE.Vector3 | { x: number, y: number, z: number }, + color?: number, + label?: string, + depthTest?: boolean, + // Y offset in world units used by updateScaleWorld only (screen-pixel API ignores this) + labelYOffset?: number, +}): WaypointSprite { + const color = options.color ?? 0xFF_00_00 + const depthTest = options.depthTest ?? false + const labelYOffset = options.labelYOffset ?? 1.5 + + // Build combined sprite + const sprite = createCombinedSprite(color, options.label ?? '', '0m', depthTest) + sprite.renderOrder = 10 + let currentLabel = options.label ?? '' + + // Offscreen arrow (detached by default) + let arrowSprite: THREE.Sprite | undefined + let arrowParent: THREE.Object3D | null = null + let arrowEnabled = WAYPOINT_CONFIG.ARROW.enabledDefault + + // Group for easy add/remove + const group = new THREE.Group() + group.add(sprite) + + // Initial position + const { x, y, z } = options.position + group.position.set(x, y, z) + + function setColor (newColor: number) { + const canvas = drawCombinedCanvas(newColor, currentLabel, '0m') + const texture = new THREE.CanvasTexture(canvas) + const mat = sprite.material + mat.map?.dispose() + mat.map = texture + mat.needsUpdate = true + } + + function setLabel (newLabel?: string) { + currentLabel = newLabel ?? '' + const canvas = drawCombinedCanvas(color, currentLabel, '0m') + const texture = new THREE.CanvasTexture(canvas) + const mat = sprite.material + mat.map?.dispose() + mat.map = texture + mat.needsUpdate = true + } + + function updateDistanceText (label: string, distanceText: string) { + const canvas = drawCombinedCanvas(color, label, distanceText) + const texture = new THREE.CanvasTexture(canvas) + const mat = sprite.material + mat.map?.dispose() + mat.map = texture + mat.needsUpdate = true + } + + function setVisible (visible: boolean) { + sprite.visible = visible + } + + function setPosition (nx: number, ny: number, nz: number) { + group.position.set(nx, ny, nz) + } + + // Keep constant pixel size on screen using global config + function updateScaleScreenPixels ( + cameraPosition: THREE.Vector3, + cameraFov: number, + distance: number, + viewportHeightPx: number + ) { + const vFovRad = cameraFov * Math.PI / 180 + const worldUnitsPerScreenHeightAtDist = Math.tan(vFovRad / 2) * 2 * distance + // Use configured target screen size + const scale = worldUnitsPerScreenHeightAtDist * (WAYPOINT_CONFIG.TARGET_SCREEN_PX / viewportHeightPx) + sprite.scale.set(scale, scale, 1) + } + + function ensureArrow () { + if (arrowSprite) return + const size = 128 + const canvas = document.createElement('canvas') + canvas.width = size + canvas.height = size + const ctx = canvas.getContext('2d')! + ctx.clearRect(0, 0, size, size) + ctx.beginPath() + ctx.moveTo(size * 0.2, size * 0.5) + ctx.lineTo(size * 0.8, size * 0.5) + ctx.lineTo(size * 0.5, size * 0.2) + ctx.closePath() + ctx.lineWidth = 4 + ctx.strokeStyle = 'black' + ctx.stroke() + ctx.fillStyle = 'white' + ctx.fill() + const texture = new THREE.CanvasTexture(canvas) + const material = new THREE.SpriteMaterial({ map: texture, transparent: true, depthTest: false, depthWrite: false }) + arrowSprite = new THREE.Sprite(material) + arrowSprite.renderOrder = 12 + arrowSprite.visible = false + if (arrowParent) arrowParent.add(arrowSprite) + } + + function enableOffscreenArrow (enabled: boolean) { + arrowEnabled = enabled + if (!enabled && arrowSprite) arrowSprite.visible = false + } + + function setArrowParent (parent: THREE.Object3D | null) { + if (arrowSprite?.parent) arrowSprite.parent.remove(arrowSprite) + arrowParent = parent + if (arrowSprite && parent) parent.add(arrowSprite) + } + + function updateOffscreenArrow ( + camera: THREE.PerspectiveCamera, + viewportWidthPx: number, + viewportHeightPx: number + ): boolean { + if (!arrowEnabled) return true + ensureArrow() + if (!arrowSprite) return true + + // Build camera basis using camera.up to respect custom orientations + const forward = new THREE.Vector3() + camera.getWorldDirection(forward) // camera look direction + const upWorld = camera.up.clone().normalize() + const right = new THREE.Vector3().copy(forward).cross(upWorld).normalize() + const upCam = new THREE.Vector3().copy(right).cross(forward).normalize() + + // Vector from camera to waypoint + const camPos = new THREE.Vector3().setFromMatrixPosition(camera.matrixWorld) + const toWp = new THREE.Vector3(group.position.x, group.position.y, group.position.z).sub(camPos) + + // Components in camera basis + const z = toWp.dot(forward) + const x = toWp.dot(right) + const y = toWp.dot(upCam) + + const aspect = viewportWidthPx / viewportHeightPx + const vFovRad = camera.fov * Math.PI / 180 + const hFovRad = 2 * Math.atan(Math.tan(vFovRad / 2) * aspect) + + // Determine if waypoint is inside view frustum using angular checks + const thetaX = Math.atan2(x, z) + const thetaY = Math.atan2(y, z) + const visible = z > 0 && Math.abs(thetaX) <= hFovRad / 2 && Math.abs(thetaY) <= vFovRad / 2 + if (visible) { + arrowSprite.visible = false + return true + } + + // Direction on screen in normalized frustum units + let rx = thetaX / (hFovRad / 2) + let ry = thetaY / (vFovRad / 2) + + // If behind the camera, snap to dominant axis to avoid confusing directions + if (z <= 0) { + if (Math.abs(rx) > Math.abs(ry)) { + rx = Math.sign(rx) + ry = 0 + } else { + rx = 0 + ry = Math.sign(ry) + } + } + + // Place on the rectangle border [-1,1]x[-1,1] + const s = Math.max(Math.abs(rx), Math.abs(ry)) || 1 + let ndcX = rx / s + let ndcY = ry / s + + // Apply padding in pixel space by clamping + const padding = WAYPOINT_CONFIG.ARROW.paddingPx + const pxX = ((ndcX + 1) * 0.5) * viewportWidthPx + const pxY = ((1 - ndcY) * 0.5) * viewportHeightPx + const clampedPxX = Math.min(Math.max(pxX, padding), viewportWidthPx - padding) + const clampedPxY = Math.min(Math.max(pxY, padding), viewportHeightPx - padding) + ndcX = (clampedPxX / viewportWidthPx) * 2 - 1 + ndcY = -(clampedPxY / viewportHeightPx) * 2 + 1 + + // Compute world position at a fixed distance in front of the camera using camera basis + const placeDist = Math.max(2, camera.near * 4) + const halfPlaneHeight = Math.tan(vFovRad / 2) * placeDist + const halfPlaneWidth = halfPlaneHeight * aspect + const pos = camPos.clone() + .add(forward.clone().multiplyScalar(placeDist)) + .add(right.clone().multiplyScalar(ndcX * halfPlaneWidth)) + .add(upCam.clone().multiplyScalar(ndcY * halfPlaneHeight)) + + // Update arrow sprite + arrowSprite.visible = true + arrowSprite.position.copy(pos) + + // Angle for rotation relative to screen right/up (derived from camera up vector) + const angle = Math.atan2(ry, rx) + arrowSprite.material.rotation = angle - Math.PI / 2 + + // Constant pixel size for arrow (use fixed placement distance) + const worldUnitsPerScreenHeightAtDist = Math.tan(vFovRad / 2) * 2 * placeDist + const sPx = worldUnitsPerScreenHeightAtDist * (WAYPOINT_CONFIG.ARROW.pixelSize / viewportHeightPx) + arrowSprite.scale.set(sPx, sPx, 1) + return false + } + + function computeDistance (cameraPosition: THREE.Vector3): number { + return cameraPosition.distanceTo(group.position) + } + + function updateForCamera ( + cameraPosition: THREE.Vector3, + camera: THREE.PerspectiveCamera, + viewportWidthPx: number, + viewportHeightPx: number + ): boolean { + const distance = computeDistance(cameraPosition) + // Keep constant pixel size + updateScaleScreenPixels(cameraPosition, camera.fov, distance, viewportHeightPx) + // Update text + updateDistanceText(currentLabel, `${Math.round(distance)}m`) + // Update arrow and visibility + const onScreen = updateOffscreenArrow(camera, viewportWidthPx, viewportHeightPx) + setVisible(onScreen) + return onScreen + } + + function dispose () { + const mat = sprite.material + mat.map?.dispose() + mat.dispose() + if (arrowSprite) { + const am = arrowSprite.material + am.map?.dispose() + am.dispose() + } + } + + return { + group, + sprite, + enableOffscreenArrow, + setArrowParent, + updateForCamera, + setColor, + setLabel, + updateDistanceText, + setVisible, + setPosition, + dispose, + } +} + +// Internal helpers +function drawCombinedCanvas (color: number, id: string, distance: string): HTMLCanvasElement { + const scale = WAYPOINT_CONFIG.CANVAS_SCALE * (globalThis.devicePixelRatio || 1) + const size = WAYPOINT_CONFIG.CANVAS_SIZE * scale + const canvas = document.createElement('canvas') + canvas.width = size + canvas.height = size + const ctx = canvas.getContext('2d')! + + // Clear canvas + ctx.clearRect(0, 0, size, size) + + // Draw dot + const centerX = size / 2 + const dotY = Math.round(size * WAYPOINT_CONFIG.LAYOUT.DOT_Y) + const radius = Math.round(size * 0.05) // Dot takes up ~12% of canvas height + const borderWidth = Math.max(2, Math.round(4 * scale)) + + // Outer border (black) + ctx.beginPath() + ctx.arc(centerX, dotY, radius + borderWidth, 0, Math.PI * 2) + ctx.fillStyle = 'black' + ctx.fill() + + // Inner circle (colored) + ctx.beginPath() + ctx.arc(centerX, dotY, radius, 0, Math.PI * 2) + ctx.fillStyle = `#${color.toString(16).padStart(6, '0')}` + ctx.fill() + + // Text properties + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + + // Title + const nameFontPx = Math.round(size * 0.08) // ~8% of canvas height + const distanceFontPx = Math.round(size * 0.06) // ~6% of canvas height + ctx.font = `bold ${nameFontPx}px mojangles` + ctx.lineWidth = Math.max(2, Math.round(3 * scale)) + const nameY = Math.round(size * WAYPOINT_CONFIG.LAYOUT.NAME_Y) + + ctx.strokeStyle = 'black' + ctx.strokeText(id, centerX, nameY) + ctx.fillStyle = 'white' + ctx.fillText(id, centerX, nameY) + + // Distance + ctx.font = `bold ${distanceFontPx}px mojangles` + ctx.lineWidth = Math.max(2, Math.round(2 * scale)) + const distanceY = Math.round(size * WAYPOINT_CONFIG.LAYOUT.DISTANCE_Y) + + ctx.strokeStyle = 'black' + ctx.strokeText(distance, centerX, distanceY) + ctx.fillStyle = '#CCCCCC' + ctx.fillText(distance, centerX, distanceY) + + return canvas +} + +function createCombinedSprite (color: number, id: string, distance: string, depthTest: boolean): THREE.Sprite { + const canvas = drawCombinedCanvas(color, id, distance) + const texture = new THREE.CanvasTexture(canvas) + texture.anisotropy = 1 + texture.magFilter = THREE.LinearFilter + texture.minFilter = THREE.LinearFilter + const material = new THREE.SpriteMaterial({ + map: texture, + transparent: true, + opacity: 1, + depthTest, + depthWrite: false, + }) + const sprite = new THREE.Sprite(material) + sprite.position.set(0, 0, 0) + return sprite +} + +export const WaypointHelpers = { + // World-scale constant size helper + computeWorldScale (distance: number, fixedReference = 10) { + return Math.max(0.0001, distance / fixedReference) + }, + // Screen-pixel constant size helper + computeScreenPixelScale ( + camera: THREE.PerspectiveCamera, + distance: number, + pixelSize: number, + viewportHeightPx: number + ) { + const vFovRad = camera.fov * Math.PI / 180 + const worldUnitsPerScreenHeightAtDist = Math.tan(vFovRad / 2) * 2 * distance + return worldUnitsPerScreenHeightAtDist * (pixelSize / viewportHeightPx) + } +} diff --git a/renderer/viewer/three/waypoints.ts b/renderer/viewer/three/waypoints.ts new file mode 100644 index 00000000..c7ec9a93 --- /dev/null +++ b/renderer/viewer/three/waypoints.ts @@ -0,0 +1,142 @@ +import * as THREE from 'three' +import { WorldRendererThree } from './worldrendererThree' +import { createWaypointSprite, type WaypointSprite } from './waypointSprite' + +interface Waypoint { + id: string + x: number + y: number + z: number + minDistance: number + color: number + label?: string + sprite: WaypointSprite +} + +interface WaypointOptions { + color?: number + label?: string + minDistance?: number +} + +export class WaypointsRenderer { + private readonly waypoints = new Map() + private readonly waypointScene = new THREE.Scene() + private readonly FIXED_REFERENCE = 10 // Fixed reference distance for scaling + + constructor ( + private readonly worldRenderer: WorldRendererThree + ) { + if (process.env.NODE_ENV !== 'production') { + this.addWaypoint('spawn', 0, 0, 0, { }) + } + } + + private updateWaypoints () { + const playerPos = this.worldRenderer.cameraObject.position + const sizeVec = this.worldRenderer.renderer.getSize(new THREE.Vector2()) + + for (const waypoint of this.waypoints.values()) { + const waypointPos = new THREE.Vector3(waypoint.x, waypoint.y, waypoint.z) + const distance = playerPos.distanceTo(waypointPos) + const visible = !waypoint.minDistance || distance >= waypoint.minDistance + + waypoint.sprite.setVisible(visible) + + if (visible) { + // Update position + waypoint.sprite.setPosition(waypoint.x, waypoint.y, waypoint.z) + // Ensure camera-based update each frame + waypoint.sprite.updateForCamera(this.worldRenderer.getCameraPosition(), this.worldRenderer.camera, sizeVec.width, sizeVec.height) + } + } + } + + render () { + if (this.waypoints.size === 0) return + + // Update waypoint scaling + this.updateWaypoints() + + // Render waypoints scene with the world camera + this.worldRenderer.renderer.render(this.waypointScene, this.worldRenderer.camera) + } + + // Removed sprite/label texture creation. Use utils/waypointSprite.ts + + addWaypoint ( + id: string, + x: number, + y: number, + z: number, + options: WaypointOptions = {} + ) { + // Remove existing waypoint if it exists + this.removeWaypoint(id) + + const color = options.color ?? 0xFF_00_00 + const { label } = options + const minDistance = options.minDistance ?? 0 + + const sprite = createWaypointSprite({ + position: new THREE.Vector3(x, y, z), + color, + label: (label || id), + }) + sprite.enableOffscreenArrow(true) + sprite.setArrowParent(this.waypointScene) + + this.waypointScene.add(sprite.group) + + this.waypoints.set(id, { + id, x, y, z, minDistance, + color, label, + sprite, + }) + } + + removeWaypoint (id: string) { + const waypoint = this.waypoints.get(id) + if (waypoint) { + this.waypointScene.remove(waypoint.sprite.group) + waypoint.sprite.dispose() + this.waypoints.delete(id) + } + } + + clear () { + for (const id of this.waypoints.keys()) { + this.removeWaypoint(id) + } + } + + testWaypoint () { + this.addWaypoint('Test Point', 0, 70, 0, { color: 0x00_FF_00, label: 'Test Point' }) + this.addWaypoint('Spawn', 0, 64, 0, { color: 0xFF_FF_00, label: 'Spawn' }) + this.addWaypoint('Far Point', 100, 70, 100, { color: 0x00_00_FF, label: 'Far Point' }) + } + + getWaypoint (id: string): Waypoint | undefined { + return this.waypoints.get(id) + } + + getAllWaypoints (): Waypoint[] { + return [...this.waypoints.values()] + } + + setWaypointColor (id: string, color: number) { + const waypoint = this.waypoints.get(id) + if (waypoint) { + waypoint.sprite.setColor(color) + waypoint.color = color + } + } + + setWaypointLabel (id: string, label?: string) { + const waypoint = this.waypoints.get(id) + if (waypoint) { + waypoint.label = label + waypoint.sprite.setLabel(label) + } + } +} diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index bc95f06b..82856cb9 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -23,6 +23,7 @@ import { ThreeJsSound } from './threeJsSound' import { CameraShake } from './cameraShake' import { ThreeJsMedia } from './threeJsMedia' import { Fountain } from './threeJsParticles' +import { WaypointsRenderer } from './waypoints' type SectionKey = string @@ -48,6 +49,7 @@ export class WorldRendererThree extends WorldRendererCommon { cameraContainer: THREE.Object3D media: ThreeJsMedia waitingChunksToDisplay = {} as { [chunkKey: string]: SectionKey[] } + waypoints: WaypointsRenderer camera: THREE.PerspectiveCamera renderTimeAvg = 0 sectionsOffsetsAnimations = {} as { @@ -99,6 +101,8 @@ export class WorldRendererThree extends WorldRendererCommon { this.soundSystem = new ThreeJsSound(this) this.cameraShake = new CameraShake(this, this.onRender) this.media = new ThreeJsMedia(this) + this.waypoints = new WaypointsRenderer(this) + // this.fountain = new Fountain(this.scene, this.scene, { // position: new THREE.Vector3(0, 10, 0), // }) @@ -119,6 +123,8 @@ export class WorldRendererThree extends WorldRendererCommon { this.protocolCustomBlocks.clear() // Reset section animations this.sectionsOffsetsAnimations = {} + // Clear waypoints + this.waypoints.clear() }) } @@ -453,7 +459,7 @@ export class WorldRendererThree extends WorldRendererCommon { return worldPos } - getWorldCameraPosition () { + getSectionCameraPosition () { const pos = this.getCameraPosition() return new Vec3( Math.floor(pos.x / 16), @@ -463,7 +469,7 @@ export class WorldRendererThree extends WorldRendererCommon { } updateCameraSectionPos () { - const newSectionPos = this.getWorldCameraPosition() + const newSectionPos = this.getSectionCameraPosition() if (!this.cameraSectionPos.equals(newSectionPos)) { this.cameraSectionPos = newSectionPos this.cameraSectionPositionUpdate() @@ -737,6 +743,8 @@ export class WorldRendererThree extends WorldRendererCommon { fountain.render() } + this.waypoints.render() + for (const onRender of this.onRender) { onRender() } @@ -1022,6 +1030,13 @@ class StarField { constructor ( private readonly worldRenderer: WorldRendererThree ) { + const clock = new THREE.Clock() + const speed = 0.2 + this.worldRenderer.onRender.push(() => { + if (!this.points) return + this.points.position.copy(this.worldRenderer.getCameraPosition()); + (this.points.material as StarfieldMaterial).uniforms.time.value = clock.getElapsedTime() * speed + }) } addToScene () { @@ -1032,7 +1047,6 @@ class StarField { const count = 7000 const factor = 7 const saturation = 10 - const speed = 0.2 const geometry = new THREE.BufferGeometry() @@ -1065,11 +1079,6 @@ class StarField { this.points = new THREE.Points(geometry, material) this.worldRenderer.scene.add(this.points) - const clock = new THREE.Clock() - this.points.onBeforeRender = (renderer, scene, camera) => { - this.points?.position.copy?.(this.worldRenderer.getCameraPosition()) - material.uniforms.time.value = clock.getElapsedTime() * speed - } this.points.renderOrder = -1 } diff --git a/src/customChannels.ts b/src/customChannels.ts index 3f6a8217..6d3aa7e9 100644 --- a/src/customChannels.ts +++ b/src/customChannels.ts @@ -16,6 +16,7 @@ export default () => { registerSectionAnimationChannels() registeredJeiChannel() registerBlockInteractionsCustomizationChannel() + registerWaypointChannels() }) } @@ -63,6 +64,62 @@ const registerBlockInteractionsCustomizationChannel = () => { }, true) } +const registerWaypointChannels = () => { + const packetStructure = [ + 'container', + [ + { + name: 'id', + type: ['pstring', { countType: 'i16' }] + }, + { + name: 'x', + type: 'f32' + }, + { + name: 'y', + type: 'f32' + }, + { + name: 'z', + type: 'f32' + }, + { + name: 'minDistance', + type: 'i32' + }, + { + name: 'label', + type: ['pstring', { countType: 'i16' }] + }, + { + name: 'color', + type: 'i32' + } + ] + ] + + registerChannel('minecraft-web-client:waypoint-add', packetStructure, (data) => { + getThreeJsRendererMethods()?.addWaypoint(data.id, data.x, data.y, data.z, { + minDistance: data.minDistance, + label: data.label || undefined, + color: data.color || undefined + }) + }) + + registerChannel('minecraft-web-client:waypoint-delete', [ + 'container', + [ + { + name: 'id', + type: ['pstring', { countType: 'i16' }] + } + ] + ], (data) => { + getThreeJsRendererMethods()?.removeWaypoint(data.id) + }) +} + const registerBlockModelsChannel = () => { const CHANNEL_NAME = 'minecraft-web-client:blockmodels' From 8827aab981d27d95b4863156a35fd66ba7b810c2 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Tue, 12 Aug 2025 06:27:42 +0300 Subject: [PATCH 131/181] dont add test waypoints on dev --- renderer/viewer/three/waypoints.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/renderer/viewer/three/waypoints.ts b/renderer/viewer/three/waypoints.ts index c7ec9a93..45937ee8 100644 --- a/renderer/viewer/three/waypoints.ts +++ b/renderer/viewer/three/waypoints.ts @@ -22,14 +22,10 @@ interface WaypointOptions { export class WaypointsRenderer { private readonly waypoints = new Map() private readonly waypointScene = new THREE.Scene() - private readonly FIXED_REFERENCE = 10 // Fixed reference distance for scaling constructor ( private readonly worldRenderer: WorldRendererThree ) { - if (process.env.NODE_ENV !== 'production') { - this.addWaypoint('spawn', 0, 0, 0, { }) - } } private updateWaypoints () { From 60fc5ef315ea8b73f58070e271dd8f3dc90fc8e4 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Wed, 13 Aug 2025 19:19:46 +0300 Subject: [PATCH 132/181] feat: add skybox renderer: test it by dragging an image window into window, fix waypoint block pos --- renderer/viewer/three/graphicsBackend.ts | 3 + renderer/viewer/three/skyboxRenderer.ts | 77 +++++++++++++++++++++ renderer/viewer/three/waypoints.ts | 2 +- renderer/viewer/three/worldrendererThree.ts | 11 +++ src/dragndrop.ts | 32 +++++++++ 5 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 renderer/viewer/three/skyboxRenderer.ts diff --git a/renderer/viewer/three/graphicsBackend.ts b/renderer/viewer/three/graphicsBackend.ts index 92b6ec66..04cb00ca 100644 --- a/renderer/viewer/three/graphicsBackend.ts +++ b/renderer/viewer/three/graphicsBackend.ts @@ -47,6 +47,9 @@ const getBackendMethods = (worldRenderer: WorldRendererThree) => { addWaypoint: worldRenderer.waypoints.addWaypoint.bind(worldRenderer.waypoints), removeWaypoint: worldRenderer.waypoints.removeWaypoint.bind(worldRenderer.waypoints), + + // New method for updating skybox + setSkyboxImage: worldRenderer.skyboxRenderer.setSkyboxImage.bind(worldRenderer.skyboxRenderer) } } diff --git a/renderer/viewer/three/skyboxRenderer.ts b/renderer/viewer/three/skyboxRenderer.ts new file mode 100644 index 00000000..294c72aa --- /dev/null +++ b/renderer/viewer/three/skyboxRenderer.ts @@ -0,0 +1,77 @@ +import * as THREE from 'three' + +export class SkyboxRenderer { + private texture: THREE.Texture | null = null + private mesh: THREE.Mesh | null = null + + constructor (private readonly scene: THREE.Scene, public initialImage: string | null) {} + + async init () { + if (this.initialImage) { + await this.setSkyboxImage(this.initialImage) + } + } + + async setSkyboxImage (imageUrl: string) { + // Dispose old textures if they exist + if (this.texture) { + this.texture.dispose() + } + + // Load the equirectangular texture + const textureLoader = new THREE.TextureLoader() + this.texture = await new Promise((resolve) => { + textureLoader.load( + imageUrl, + (texture) => { + texture.mapping = THREE.EquirectangularReflectionMapping + texture.encoding = THREE.sRGBEncoding + // Keep pixelated look + texture.minFilter = THREE.NearestFilter + texture.magFilter = THREE.NearestFilter + texture.needsUpdate = true + resolve(texture) + } + ) + }) + + // Create or update the skybox + if (this.mesh) { + // Just update the texture on the existing material + this.mesh.material.map = this.texture + this.mesh.material.needsUpdate = true + } else { + // Create a large sphere geometry for the skybox + const geometry = new THREE.SphereGeometry(500, 60, 40) + // Flip the geometry inside out + geometry.scale(-1, 1, 1) + + // Create material using the loaded texture + const material = new THREE.MeshBasicMaterial({ + map: this.texture, + side: THREE.FrontSide // Changed to FrontSide since we're flipping the geometry + }) + + // Create and add the skybox mesh + this.mesh = new THREE.Mesh(geometry, material) + this.scene.add(this.mesh) + } + } + + update (cameraPosition: THREE.Vector3) { + if (this.mesh) { + this.mesh.position.copy(cameraPosition) + } + } + + dispose () { + if (this.texture) { + this.texture.dispose() + } + if (this.mesh) { + this.mesh.geometry.dispose() + ;(this.mesh.material as THREE.Material).dispose() + this.scene.remove(this.mesh) + } + } +} diff --git a/renderer/viewer/three/waypoints.ts b/renderer/viewer/three/waypoints.ts index 45937ee8..cebd779a 100644 --- a/renderer/viewer/three/waypoints.ts +++ b/renderer/viewer/three/waypoints.ts @@ -85,7 +85,7 @@ export class WaypointsRenderer { this.waypointScene.add(sprite.group) this.waypoints.set(id, { - id, x, y, z, minDistance, + id, x: x + 0.5, y: y + 0.5, z: z + 0.5, minDistance, color, label, sprite, }) diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index 82856cb9..fb6c8e11 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -24,6 +24,7 @@ import { CameraShake } from './cameraShake' import { ThreeJsMedia } from './threeJsMedia' import { Fountain } from './threeJsParticles' import { WaypointsRenderer } from './waypoints' +import { SkyboxRenderer } from './skyboxRenderer' type SectionKey = string @@ -71,6 +72,7 @@ export class WorldRendererThree extends WorldRendererCommon { } fountains: Fountain[] = [] DEBUG_RAYCAST = false + skyboxRenderer: SkyboxRenderer private currentPosTween?: tweenJs.Tween private currentRotTween?: tweenJs.Tween<{ pitch: number, yaw: number }> @@ -94,6 +96,10 @@ export class WorldRendererThree extends WorldRendererCommon { this.holdingBlock = new HoldingBlock(this) this.holdingBlockLeft = new HoldingBlock(this, true) + // Initialize skybox renderer + this.skyboxRenderer = new SkyboxRenderer(this.scene, null) + void this.skyboxRenderer.init() + this.addDebugOverlay() this.resetScene() void this.init() @@ -708,6 +714,10 @@ export class WorldRendererThree extends WorldRendererCommon { this.cursorBlock.render() this.updateSectionOffsets() + // Update skybox position to follow camera + const cameraPos = this.getCameraPosition() + this.skyboxRenderer.update(cameraPos) + const sizeOrFovChanged = sizeChanged || this.displayOptions.inWorldRenderingConfig.fov !== this.camera.fov if (sizeOrFovChanged) { const size = this.renderer.getSize(new THREE.Vector2()) @@ -947,6 +957,7 @@ export class WorldRendererThree extends WorldRendererCommon { destroy (): void { super.destroy() + this.skyboxRenderer.dispose() } shouldObjectVisible (object: THREE.Object3D) { diff --git a/src/dragndrop.ts b/src/dragndrop.ts index 6be90551..5a16bc05 100644 --- a/src/dragndrop.ts +++ b/src/dragndrop.ts @@ -3,6 +3,7 @@ import fs from 'fs' import * as nbt from 'prismarine-nbt' import RegionFile from 'prismarine-provider-anvil/src/region' import { versions } from 'minecraft-data' +import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods' import { openWorldDirectory, openWorldZip } from './browserfs' import { isGameActive } from './globalState' import { showNotification } from './react/NotificationProvider' @@ -12,6 +13,9 @@ const parseNbt = promisify(nbt.parse) const simplifyNbt = nbt.simplify window.nbt = nbt +// Supported image types for skybox +const VALID_IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.webp'] + // todo display drop zone for (const event of ['drag', 'dragstart', 'dragend', 'dragover', 'dragenter', 'dragleave', 'drop']) { window.addEventListener(event, (e: any) => { @@ -45,6 +49,34 @@ window.addEventListener('drop', async e => { }) async function handleDroppedFile (file: File) { + // Check for image files first when game is active + if (isGameActive(false) && VALID_IMAGE_EXTENSIONS.some(ext => file.name.toLowerCase().endsWith(ext))) { + try { + // Convert image to base64 + const reader = new FileReader() + const base64Promise = new Promise((resolve, reject) => { + reader.onload = () => resolve(reader.result as string) + reader.onerror = reject + }) + reader.readAsDataURL(file) + const base64Image = await base64Promise + + // Get ThreeJS backend methods and update skybox + const setSkyboxImage = getThreeJsRendererMethods()?.setSkyboxImage + if (setSkyboxImage) { + await setSkyboxImage(base64Image) + showNotification('Skybox updated successfully') + } else { + showNotification('Cannot update skybox - renderer does not support it') + } + return + } catch (err) { + console.error('Failed to update skybox:', err) + showNotification('Failed to update skybox', 'error') + return + } + } + if (file.name.endsWith('.zip')) { void openWorldZip(file) return From 15e3325971ed85ee54d5a3022b789a8881a7f4e4 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Thu, 14 Aug 2025 01:25:24 +0300 Subject: [PATCH 133/181] add param for testing for immediate reconnect after kick or error (warning: will cause infinite reload loop) --- src/appParams.ts | 1 + src/index.ts | 13 +++++++++---- src/react/AppStatusProvider.tsx | 24 ++++++++++++++---------- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/appParams.ts b/src/appParams.ts index 59a24788..8d487f8d 100644 --- a/src/appParams.ts +++ b/src/appParams.ts @@ -12,6 +12,7 @@ export type AppQsParams = { username?: string lockConnect?: string autoConnect?: string + alwaysReconnect?: string // googledrive.ts params state?: string // ServersListProvider.tsx params diff --git a/src/index.ts b/src/index.ts index c0213f56..4a118cee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -62,7 +62,7 @@ import { onAppLoad, resourcepackReload, resourcePackState } from './resourcePack import { ConnectPeerOptions, connectToPeer } from './localServerMultiplayer' import CustomChannelClient from './customClient' import { registerServiceWorker } from './serviceWorker' -import { appStatusState, lastConnectOptions } from './react/AppStatusProvider' +import { appStatusState, lastConnectOptions, quickDevReconnect } from './react/AppStatusProvider' import { fsState } from './loadSave' import { watchFov } from './rendererUtils' @@ -215,8 +215,13 @@ export async function connect (connectOptions: ConnectOptions) { const destroyAll = (wasKicked = false) => { if (ended) return loadingTimerState.loading = false - if (!wasKicked && miscUiState.appConfig?.allowAutoConnect && appQueryParams.autoConnect && hadConnected) { - location.reload() + const { alwaysReconnect } = appQueryParams + if ((!wasKicked && miscUiState.appConfig?.allowAutoConnect && appQueryParams.autoConnect && hadConnected) || (alwaysReconnect)) { + if (alwaysReconnect === 'quick' || alwaysReconnect === 'fast') { + quickDevReconnect() + } else { + location.reload() + } } errorAbortController.abort() ended = true @@ -957,7 +962,7 @@ const maybeEnterGame = () => { } } - if (appQueryParams.reconnect && process.env.NODE_ENV === 'development') { + if (appQueryParams.reconnect && localStorage.lastConnectOptions && process.env.NODE_ENV === 'development') { const lastConnect = JSON.parse(localStorage.lastConnectOptions ?? {}) return waitForConfigFsLoad(async () => { void connect({ diff --git a/src/react/AppStatusProvider.tsx b/src/react/AppStatusProvider.tsx index e7e36cb7..9c7b34ac 100644 --- a/src/react/AppStatusProvider.tsx +++ b/src/react/AppStatusProvider.tsx @@ -54,6 +54,17 @@ export const reconnectReload = () => { } } +export const quickDevReconnect = () => { + if (!lastConnectOptions.value) { + return + } + + resetAppStatusState() + window.dispatchEvent(new window.CustomEvent('connect', { + detail: lastConnectOptions.value + })) +} + export default () => { const lastState = useRef(JSON.parse(JSON.stringify(appStatusState))) const currentState = useSnapshot(appStatusState) @@ -105,13 +116,6 @@ export default () => { } }, [isOpen]) - const reconnect = () => { - resetAppStatusState() - window.dispatchEvent(new window.CustomEvent('connect', { - detail: lastConnectOptions.value - })) - } - useEffect(() => { const controller = new AbortController() window.addEventListener('keyup', (e) => { @@ -119,7 +123,7 @@ export default () => { if (activeModalStack.at(-1)?.reactType !== 'app-status') return // todo do only if reconnect is possible if (e.code !== 'KeyR' || !lastConnectOptions.value) return - reconnect() + quickDevReconnect() }, { signal: controller.signal }) @@ -140,7 +144,7 @@ export default () => { const account = await showOptionsModal('Choose account to connect with', [...accounts.map(account => account.username), 'Use other account']) if (!account) return lastConnectOptions.value!.authenticatedAccount = accounts.find(acc => acc.username === account) || true - reconnect() + quickDevReconnect() } const lastAutoCapturedPackets = getLastAutoCapturedPackets() @@ -184,7 +188,7 @@ export default () => { actionsSlot={ <> {displayAuthButton &&
    ) })} diff --git a/src/react/ChunksDebugScreen.tsx b/src/react/ChunksDebugScreen.tsx index de33e454..28b0bbc4 100644 --- a/src/react/ChunksDebugScreen.tsx +++ b/src/react/ChunksDebugScreen.tsx @@ -2,6 +2,8 @@ import { useEffect, useState } from 'react' import { useUtilsEffect } from '@zardoy/react-util' import { WorldRendererCommon } from 'renderer/viewer/lib/worldrendererCommon' import { WorldRendererThree } from 'renderer/viewer/three/worldrendererThree' +import { Vec3 } from 'vec3' +import { generateSpiralMatrix } from 'flying-squid/dist/utils' import Screen from './Screen' import ChunksDebug, { ChunkDebug } from './ChunksDebug' import { useIsModalActive } from './utilsApp' @@ -12,6 +14,10 @@ const Inner = () => { const [update, setUpdate] = useState(0) useUtilsEffect(({ interval }) => { + const up = () => { + // setUpdate(u => u + 1) + } + bot.on('chunkColumnLoad', up) interval( 500, () => { @@ -20,15 +26,46 @@ const Inner = () => { setUpdate(u => u + 1) } ) + return () => { + bot.removeListener('chunkColumnLoad', up) + } }, []) + // Track first load time for all chunks + const allLoadTimes = Object.values(worldView!.debugChunksInfo) + .map(chunk => chunk?.loads[0]?.time ?? Infinity) + .filter(time => time !== Infinity) + .sort((a, b) => a - b) + + const allSpiralChunks = Object.fromEntries(generateSpiralMatrix(worldView!.viewDistance).map(pos => [`${pos[0]},${pos[1]}`, pos])) + const mapChunk = (key: string, state: ChunkDebug['state']): ChunkDebug => { + const x = Number(key.split(',')[0]) + const z = Number(key.split(',')[1]) + const chunkX = Math.floor(x / 16) + const chunkZ = Math.floor(z / 16) + + delete allSpiralChunks[`${chunkX},${chunkZ}`] const chunk = worldView!.debugChunksInfo[key] + const firstLoadTime = chunk?.loads[0]?.time + const loadIndex = firstLoadTime ? allLoadTimes.indexOf(firstLoadTime) + 1 : 0 + // const timeSinceFirstLoad = firstLoadTime ? firstLoadTime - allLoadTimes[0] : 0 + const timeSinceFirstLoad = firstLoadTime ? firstLoadTime - allLoadTimes[0] : 0 + let line = '' + let line2 = '' + if (loadIndex) { + line = `${loadIndex}` + line2 = `${timeSinceFirstLoad}ms` + } + if (chunk?.loads.length > 1) { + line += ` - ${chunk.loads.length}` + } + return { - x: Number(key.split(',')[0]), - z: Number(key.split(',')[1]), + x, + z, state, - lines: [String(chunk?.loads.length ?? 0)], + lines: [line, line2], sidebarLines: [ `loads: ${chunk?.loads?.map(l => `${l.reason} ${l.dataLength} ${l.time}`).join('\n')}`, // `blockUpdates: ${chunk.blockUpdates}`, @@ -55,14 +92,22 @@ const Inner = () => { const chunksDone = Object.keys(world.finishedChunks).map(key => mapChunk(key, 'done')) + + const chunksWaitingOrder = Object.values(allSpiralChunks).map(([x, z]) => { + const pos = new Vec3(x * 16, 0, z * 16) + if (bot.world.getColumnAt(pos) === null) return null + return mapChunk(`${pos.x},${pos.z}`, 'order-queued') + }).filter(a => !!a) + const allChunks = [ ...chunksWaitingServer, ...chunksWaitingClient, ...clientProcessingChunks, ...chunksDone, ...chunksDoneEmpty, + ...chunksWaitingOrder, ] - return + return Date: Sat, 16 Aug 2025 09:15:37 +0300 Subject: [PATCH 139/181] - Introduced a patchAssets script to apply custom textures to the blocks and items atlases. - Enhanced the ThreeJsSound class to support sound playback timeout and volume adjustments. - Added a custom sound system to handle named sound effects with metadata. --- assets/customTextures/readme.md | 2 + renderer/viewer/three/threeJsSound.ts | 40 ++++++-- rsbuild.config.ts | 4 + scripts/patchAssets.ts | 137 ++++++++++++++++++++++++++ src/basicSounds.ts | 30 +++++- src/defaultOptions.ts | 1 + src/react/Slider.tsx | 58 ++++++++++- src/sounds/botSoundSystem.ts | 11 ++- src/sounds/customSoundSystem.ts | 44 +++++++++ src/sounds/soundsMap.ts | 10 +- src/watchOptions.ts | 4 + 11 files changed, 323 insertions(+), 18 deletions(-) create mode 100644 assets/customTextures/readme.md create mode 100644 scripts/patchAssets.ts create mode 100644 src/sounds/customSoundSystem.ts diff --git a/assets/customTextures/readme.md b/assets/customTextures/readme.md new file mode 100644 index 00000000..e2a78c20 --- /dev/null +++ b/assets/customTextures/readme.md @@ -0,0 +1,2 @@ +here you can place custom textures for bundled files (blocks/items) e.g. blocks/stone.png +get file names from here (blocks/items) https://zardoy.github.io/mc-assets/ diff --git a/renderer/viewer/three/threeJsSound.ts b/renderer/viewer/three/threeJsSound.ts index 46aefda9..699bb2cc 100644 --- a/renderer/viewer/three/threeJsSound.ts +++ b/renderer/viewer/three/threeJsSound.ts @@ -2,7 +2,7 @@ import * as THREE from 'three' import { WorldRendererThree } from './worldrendererThree' export interface SoundSystem { - playSound: (position: { x: number, y: number, z: number }, path: string, volume?: number, pitch?: number) => void + playSound: (position: { x: number, y: number, z: number }, path: string, volume?: number, pitch?: number, timeout?: number) => void destroy: () => void } @@ -10,7 +10,17 @@ export class ThreeJsSound implements SoundSystem { audioListener: THREE.AudioListener | undefined private readonly activeSounds = new Set() private readonly audioContext: AudioContext | undefined + private readonly soundVolumes = new Map() + baseVolume = 1 + constructor (public worldRenderer: WorldRendererThree) { + worldRenderer.onWorldSwitched.push(() => { + this.stopAll() + }) + + worldRenderer.onReactiveConfigUpdated('volume', (volume) => { + this.changeVolume(volume) + }) } initAudioListener () { @@ -19,20 +29,24 @@ export class ThreeJsSound implements SoundSystem { this.worldRenderer.camera.add(this.audioListener) } - playSound (position: { x: number, y: number, z: number }, path: string, volume = 1, pitch = 1) { + playSound (position: { x: number, y: number, z: number }, path: string, volume = 1, pitch = 1, timeout = 500) { this.initAudioListener() const sound = new THREE.PositionalAudio(this.audioListener!) this.activeSounds.add(sound) + this.soundVolumes.set(sound, volume) const audioLoader = new THREE.AudioLoader() const start = Date.now() void audioLoader.loadAsync(path).then((buffer) => { - if (Date.now() - start > 500) return + if (Date.now() - start > timeout) { + console.warn('Ignored playing sound', path, 'due to timeout:', timeout, 'ms <', Date.now() - start, 'ms') + return + } // play sound.setBuffer(buffer) sound.setRefDistance(20) - sound.setVolume(volume) + sound.setVolume(volume * this.baseVolume) sound.setPlaybackRate(pitch) // set the pitch this.worldRenderer.scene.add(sound) // set sound position @@ -43,21 +57,35 @@ export class ThreeJsSound implements SoundSystem { sound.disconnect() } this.activeSounds.delete(sound) + this.soundVolumes.delete(sound) audioLoader.manager.itemEnd(path) } sound.play() }) } - destroy () { - // Stop and clean up all active sounds + stopAll () { for (const sound of this.activeSounds) { + if (!sound) continue sound.stop() if (sound.source) { sound.disconnect() } + this.worldRenderer.scene.remove(sound) } + this.activeSounds.clear() + this.soundVolumes.clear() + } + changeVolume (volume: number) { + this.baseVolume = volume + for (const [sound, individualVolume] of this.soundVolumes) { + sound.setVolume(individualVolume * this.baseVolume) + } + } + + destroy () { + this.stopAll() // Remove and cleanup audio listener if (this.audioListener) { this.audioListener.removeFromParent() diff --git a/rsbuild.config.ts b/rsbuild.config.ts index 42d6867b..6cd6b2ed 100644 --- a/rsbuild.config.ts +++ b/rsbuild.config.ts @@ -240,6 +240,10 @@ const appConfig = defineConfig({ prep() }) build.onAfterBuild(async () => { + if (fs.readdirSync('./assets/customTextures').length > 0) { + childProcess.execSync('tsx ./scripts/patchAssets.ts', { stdio: 'inherit' }) + } + if (SINGLE_FILE_BUILD) { // check that only index.html is in the dist/single folder const singleBuildFiles = fs.readdirSync('./dist/single') diff --git a/scripts/patchAssets.ts b/scripts/patchAssets.ts new file mode 100644 index 00000000..99994f5f --- /dev/null +++ b/scripts/patchAssets.ts @@ -0,0 +1,137 @@ +import blocksAtlas from 'mc-assets/dist/blocksAtlases.json' +import itemsAtlas from 'mc-assets/dist/itemsAtlases.json' +import * as fs from 'fs' +import * as path from 'path' +import sharp from 'sharp' + +interface AtlasFile { + latest: { + suSv: number + tileSize: number + width: number + height: number + textures: { + [key: string]: { + u: number + v: number + su: number + sv: number + tileIndex: number + } + } + } +} + +async function patchTextureAtlas( + atlasType: 'blocks' | 'items', + atlasData: AtlasFile, + customTexturesDir: string, + distDir: string +) { + // Check if custom textures directory exists and has files + if (!fs.existsSync(customTexturesDir) || fs.readdirSync(customTexturesDir).length === 0) { + return + } + + // Find the latest atlas file + const atlasFiles = fs.readdirSync(distDir) + .filter(file => file.startsWith(`${atlasType}AtlasLatest`) && file.endsWith('.png')) + .sort() + + if (atlasFiles.length === 0) { + console.log(`No ${atlasType}AtlasLatest.png found in ${distDir}`) + return + } + + const latestAtlasFile = atlasFiles[atlasFiles.length - 1] + const atlasPath = path.join(distDir, latestAtlasFile) + console.log(`Patching ${atlasPath}`) + + // Get atlas dimensions + const atlasMetadata = await sharp(atlasPath).metadata() + if (!atlasMetadata.width || !atlasMetadata.height) { + throw new Error(`Failed to get atlas dimensions for ${atlasPath}`) + } + + // Process each custom texture + const customTextureFiles = fs.readdirSync(customTexturesDir) + .filter(file => file.endsWith('.png')) + + if (customTextureFiles.length === 0) return + + // Prepare composite operations + const composites: sharp.OverlayOptions[] = [] + + for (const textureFile of customTextureFiles) { + const textureName = path.basename(textureFile, '.png') + + if (atlasData.latest.textures[textureName]) { + const textureData = atlasData.latest.textures[textureName] + const customTexturePath = path.join(customTexturesDir, textureFile) + + try { + // Convert UV coordinates to pixel coordinates + const x = Math.round(textureData.u * atlasMetadata.width) + const y = Math.round(textureData.v * atlasMetadata.height) + const width = Math.round((textureData.su ?? atlasData.latest.suSv) * atlasMetadata.width) + const height = Math.round((textureData.sv ?? atlasData.latest.suSv) * atlasMetadata.height) + + // Resize custom texture to match atlas dimensions and add to composite operations + const resizedTextureBuffer = await sharp(customTexturePath) + .resize(width, height, { + fit: 'fill', + kernel: 'nearest' // Preserve pixel art quality + }) + .png() + .toBuffer() + + composites.push({ + input: resizedTextureBuffer, + left: x, + top: y, + blend: 'over' + }) + + console.log(`Prepared ${textureName} at (${x}, ${y}) with size (${width}, ${height})`) + } catch (error) { + console.error(`Failed to prepare ${textureName}:`, error) + } + } else { + console.warn(`Texture ${textureName} not found in ${atlasType} atlas`) + } + } + + if (composites.length > 0) { + // Apply all patches at once using Sharp's composite + await sharp(atlasPath) + .composite(composites) + .png() + .toFile(atlasPath + '.tmp') + + // Replace original with patched version + fs.renameSync(atlasPath + '.tmp', atlasPath) + console.log(`Saved patched ${atlasType} atlas to ${atlasPath}`) + } +} + +async function main() { + const customBlocksDir = './assets/customTextures/blocks' + const customItemsDir = './assets/customTextures/items' + const distDir = './dist/static/image' + + try { + // Patch blocks atlas + await patchTextureAtlas('blocks', blocksAtlas as unknown as AtlasFile, customBlocksDir, distDir) + + // Patch items atlas + await patchTextureAtlas('items', itemsAtlas as unknown as AtlasFile, customItemsDir, distDir) + + console.log('Texture atlas patching completed!') + } catch (error) { + console.error('Failed to patch texture atlases:', error) + process.exit(1) + } +} + +// Run the script +main() diff --git a/src/basicSounds.ts b/src/basicSounds.ts index 40428c6b..37f8dccd 100644 --- a/src/basicSounds.ts +++ b/src/basicSounds.ts @@ -43,7 +43,7 @@ export async function loadSound (path: string, contents = path) { } } -export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = 500) => { +export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = options.remoteSoundsLoadTimeout, loop = false) => { const soundBuffer = sounds[url] if (!soundBuffer) { const start = Date.now() @@ -51,10 +51,10 @@ export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = 500) = if (cancelled || Date.now() - start > loadTimeout) return } - return playSound(url, soundVolume) + return playSound(url, soundVolume, loop) } -export async function playSound (url, soundVolume = 1) { +export async function playSound (url, soundVolume = 1, loop = false) { const volume = soundVolume * (options.volume / 100) if (!volume) return @@ -75,6 +75,7 @@ export async function playSound (url, soundVolume = 1) { const gainNode = audioContext.createGain() const source = audioContext.createBufferSource() source.buffer = soundBuffer + source.loop = loop source.connect(gainNode) gainNode.connect(audioContext.destination) gainNode.gain.value = volume @@ -99,6 +100,16 @@ export async function playSound (url, soundVolume = 1) { onEnded (callback: () => void) { callbacks.push(callback) }, + stop () { + try { + source.stop() + // Remove from active sounds + const index = activeSounds.findIndex(s => s.source === source) + if (index !== -1) activeSounds.splice(index, 1) + } catch (err) { + console.warn('Failed to stop sound:', err) + } + }, } } @@ -113,6 +124,19 @@ export function stopAllSounds () { activeSounds.length = 0 } +export function stopSound (url: string) { + const soundIndex = activeSounds.findIndex(s => s.source.buffer === sounds[url]) + if (soundIndex !== -1) { + const { source } = activeSounds[soundIndex] + try { + source.stop() + } catch (err) { + console.warn('Failed to stop sound:', err) + } + activeSounds.splice(soundIndex, 1) + } +} + export function changeVolumeOfCurrentlyPlayingSounds (newVolume: number) { const normalizedVolume = newVolume / 100 for (const { gainNode, volumeMultiplier } of activeSounds) { diff --git a/src/defaultOptions.ts b/src/defaultOptions.ts index 95dbfd63..378cd14a 100644 --- a/src/defaultOptions.ts +++ b/src/defaultOptions.ts @@ -85,6 +85,7 @@ export const defaultOptions = { } as any, preferLoadReadonly: false, experimentalClientSelfReload: true, + remoteSoundsLoadTimeout: 500, disableLoadPrompts: false, guestUsername: 'guest', askGuestName: true, diff --git a/src/react/Slider.tsx b/src/react/Slider.tsx index e177578c..2a068264 100644 --- a/src/react/Slider.tsx +++ b/src/react/Slider.tsx @@ -1,5 +1,5 @@ // Slider.tsx -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useRef, useCallback } from 'react' import styles from './slider.module.css' import SharedHudVars from './SharedHudVars' @@ -12,6 +12,7 @@ interface Props extends React.ComponentProps<'div'> { min?: number; max?: number; disabledReason?: string; + throttle?: number | false; // milliseconds, default 100, false to disable updateValue?: (value: number) => void; updateOnDragEnd?: boolean; @@ -26,15 +27,24 @@ const Slider: React.FC = ({ min = 0, max = 100, disabledReason, + throttle = 0, updateOnDragEnd = false, updateValue, ...divProps }) => { + label = translate(label) + disabledReason = translate(disabledReason) + valueDisplay = typeof valueDisplay === 'string' ? translate(valueDisplay) : valueDisplay + const [value, setValue] = useState(valueProp) const getRatio = (v = value) => Math.max(Math.min((v - min) / (max - min), 1), 0) const [ratio, setRatio] = useState(getRatio()) + // Throttling refs + const timeoutRef = useRef(null) + const lastValueRef = useRef(valueProp) + useEffect(() => { setValue(valueProp) }, [valueProp]) @@ -42,14 +52,52 @@ const Slider: React.FC = ({ setRatio(getRatio()) }, [value, min, max]) - const fireValueUpdate = (dragEnd: boolean, v = value) => { + const throttledUpdateValue = useCallback((newValue: number, dragEnd: boolean) => { if (updateOnDragEnd !== dragEnd) return - updateValue?.(v) + if (!updateValue) return + + lastValueRef.current = newValue + + if (!throttle) { + // No throttling + updateValue(newValue) + return + } + + // Clear existing timeout + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + } + + // Set new timeout + timeoutRef.current = setTimeout(() => { + updateValue(lastValueRef.current) + timeoutRef.current = null + }, throttle) + }, [updateValue, updateOnDragEnd, throttle]) + + // Cleanup timeout on unmount + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + // Fire the last value immediately on cleanup + if (updateValue && lastValueRef.current !== undefined) { + updateValue(lastValueRef.current) + } + } + } + }, [updateValue]) + + const fireValueUpdate = (dragEnd: boolean, v = value) => { + throttledUpdateValue(v, dragEnd) } + const labelText = `${label}: ${valueDisplay ?? value} ${unit}` + return ( -
    +
    17 ? 'settings-text-container-long' : ''}`} style={{ width }} {...divProps}> = ({
    diff --git a/src/sounds/botSoundSystem.ts b/src/sounds/botSoundSystem.ts index 72aa3da8..cb237072 100644 --- a/src/sounds/botSoundSystem.ts +++ b/src/sounds/botSoundSystem.ts @@ -11,6 +11,7 @@ import { showNotification } from '../react/NotificationProvider' import { pixelartIcons } from '../react/PixelartIcon' import { createSoundMap, SoundMap } from './soundsMap' import { musicSystem } from './musicSystem' +import './customSoundSystem' let soundMap: SoundMap | undefined @@ -50,8 +51,9 @@ subscribeKey(miscUiState, 'gameLoaded', async () => { appViewer.backend?.soundSystem?.playSound( position, soundData.url, - soundData.volume * (options.volume / 100), - Math.max(Math.min(pitch ?? 1, 2), 0.5) + soundData.volume, + Math.max(Math.min(pitch ?? 1, 2), 0.5), + soundData.timeout ?? options.remoteSoundsLoadTimeout ) } if (getDistance(bot.entity.position, position) < 4 * 16) { @@ -81,7 +83,7 @@ subscribeKey(miscUiState, 'gameLoaded', async () => { } const randomMusicKey = musicKeys[Math.floor(Math.random() * musicKeys.length)] const soundData = await soundMap.getSoundUrl(randomMusicKey) - if (!soundData) return + if (!soundData || !soundMap) return await musicSystem.playMusic(soundData.url, soundData.volume) } @@ -109,6 +111,9 @@ subscribeKey(miscUiState, 'gameLoaded', async () => { } bot.on('soundEffectHeard', async (soundId, position, volume, pitch) => { + if (/^https?:/.test(soundId.replace('minecraft:', ''))) { + return + } await playHardcodedSound(soundId, position, volume, pitch) }) diff --git a/src/sounds/customSoundSystem.ts b/src/sounds/customSoundSystem.ts new file mode 100644 index 00000000..5dfa89f7 --- /dev/null +++ b/src/sounds/customSoundSystem.ts @@ -0,0 +1,44 @@ +import { loadOrPlaySound, stopAllSounds, stopSound } from '../basicSounds' + +const customSoundSystem = () => { + bot._client.on('named_sound_effect', packet => { + let { soundName } = packet + let metadata = {} as { loadTimeout?: number, loop?: boolean } + + // Extract JSON metadata from parentheses at the end + const jsonMatch = /\(({.*})\)$/.exec(soundName) + if (jsonMatch) { + try { + metadata = JSON.parse(jsonMatch[1]) + soundName = soundName.slice(0, -jsonMatch[0].length) + } catch (e) { + console.warn('Failed to parse sound metadata:', jsonMatch[1]) + } + } + + if (/^https?:/.test(soundName.replace('minecraft:', ''))) { + const { loadTimeout, loop } = metadata + void loadOrPlaySound(soundName, packet.volume, loadTimeout, loop) + } + }) + + bot._client.on('stop_sound', packet => { + const { flags, source, sound } = packet + + if (flags === 0) { + // Stop all sounds + stopAllSounds() + } else if (sound) { + // Stop specific sound by name + stopSound(sound) + } + }) + + bot.on('end', () => { + stopAllSounds() + }) +} + +customEvents.on('mineflayerBotCreated', () => { + customSoundSystem() +}) diff --git a/src/sounds/soundsMap.ts b/src/sounds/soundsMap.ts index 1b0a0178..47028971 100644 --- a/src/sounds/soundsMap.ts +++ b/src/sounds/soundsMap.ts @@ -35,6 +35,7 @@ interface ResourcePackSoundEntry { name: string stream?: boolean volume?: number + timeout?: number } interface ResourcePackSound { @@ -140,7 +141,7 @@ export class SoundMap { await scan(soundsBasePath) } - async getSoundUrl (soundKey: string, volume = 1): Promise<{ url: string; volume: number } | undefined> { + async getSoundUrl (soundKey: string, volume = 1): Promise<{ url: string; volume: number, timeout?: number } | undefined> { // First check resource pack sounds.json if (this.activeResourcePackSoundsJson && soundKey in this.activeResourcePackSoundsJson) { const rpSound = this.activeResourcePackSoundsJson[soundKey] @@ -151,6 +152,13 @@ export class SoundMap { if (this.activeResourcePackBasePath) { const tryFormat = async (format: string) => { try { + if (sound.name.startsWith('http://') || sound.name.startsWith('https://')) { + return { + url: sound.name, + volume: soundVolume * Math.max(Math.min(volume, 1), 0), + timeout: sound.timeout + } + } const resourcePackPath = path.join(this.activeResourcePackBasePath!, `/assets/minecraft/sounds/${sound.name}.${format}`) const fileData = await fs.promises.readFile(resourcePackPath) return { diff --git a/src/watchOptions.ts b/src/watchOptions.ts index 478da4fb..2f5199c0 100644 --- a/src/watchOptions.ts +++ b/src/watchOptions.ts @@ -80,6 +80,10 @@ export const watchOptionsAfterViewerInit = () => { updateFpsLimit(o) }) + watchValue(options, o => { + appViewer.inWorldRenderingConfig.volume = Math.max(o.volume / 100, 0) + }) + watchValue(options, o => { appViewer.inWorldRenderingConfig.vrSupport = o.vrSupport appViewer.inWorldRenderingConfig.vrPageGameRendering = o.vrPageGameRendering From d6eb1601e918b179b11ed2898df2befbd7d93d2d Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sat, 16 Aug 2025 09:16:29 +0300 Subject: [PATCH 140/181] disable remote sounds by default --- src/defaultOptions.ts | 1 + src/sounds/customSoundSystem.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/defaultOptions.ts b/src/defaultOptions.ts index 378cd14a..28b64059 100644 --- a/src/defaultOptions.ts +++ b/src/defaultOptions.ts @@ -85,6 +85,7 @@ export const defaultOptions = { } as any, preferLoadReadonly: false, experimentalClientSelfReload: true, + remoteSoundsSupport: false, remoteSoundsLoadTimeout: 500, disableLoadPrompts: false, guestUsername: 'guest', diff --git a/src/sounds/customSoundSystem.ts b/src/sounds/customSoundSystem.ts index 5dfa89f7..1880aa70 100644 --- a/src/sounds/customSoundSystem.ts +++ b/src/sounds/customSoundSystem.ts @@ -1,7 +1,9 @@ import { loadOrPlaySound, stopAllSounds, stopSound } from '../basicSounds' +import { options } from '../optionsStorage' const customSoundSystem = () => { bot._client.on('named_sound_effect', packet => { + if (!options.remoteSoundsSupport) return let { soundName } = packet let metadata = {} as { loadTimeout?: number, loop?: boolean } From 9a84a7acfba4a9a7c56fdb4612fa2e35818ae085 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Mon, 18 Aug 2025 11:37:20 +0300 Subject: [PATCH 141/181] do less annoying logging --- renderer/viewer/lib/worldDataEmitter.ts | 1 - src/appViewer.ts | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/renderer/viewer/lib/worldDataEmitter.ts b/renderer/viewer/lib/worldDataEmitter.ts index 3bc2acd3..86a85f77 100644 --- a/renderer/viewer/lib/worldDataEmitter.ts +++ b/renderer/viewer/lib/worldDataEmitter.ts @@ -379,7 +379,6 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter { if (!options.experimentalClientSelfReload) return - displayClientChat(`[client] client panicked due to too long loading time. Soft reloading chunks...`) + if (process.env.NODE_ENV === 'development') { + displayClientChat(`[client] client panicked due to too long loading time. Soft reloading chunks...`) + } void reloadChunks() } window.worldView = this.worldView From a8fa3d47d1e43d95313cd19ea1838678e423ba21 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Tue, 19 Aug 2025 12:49:33 +0300 Subject: [PATCH 142/181] up protocol & mineflayer for 1.21.6 --- pnpm-lock.yaml | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f495c831..e7cb1872 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -137,13 +137,13 @@ importers: version: 4.17.21 mcraft-fun-mineflayer: specifier: ^0.1.23 - version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/8aaec9f31657ebb5195cc13db09e5fc6760d4a09(encoding@0.1.13)) + version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13)) minecraft-data: specifier: 3.92.0 version: 3.92.0 minecraft-protocol: specifier: github:PrismarineJS/node-minecraft-protocol#master - version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=2a88e61fea1825d9fa5f1584fde810421d553d23e45e6dc829c3697ee3358bea)(encoding@0.1.13) + version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=2a88e61fea1825d9fa5f1584fde810421d553d23e45e6dc829c3697ee3358bea)(encoding@0.1.13) mineflayer-item-map-downloader: specifier: github:zardoy/mineflayer-item-map-downloader version: https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad)(encoding@0.1.13) @@ -342,7 +342,7 @@ importers: version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/89c33d396f3fde4804c71f4be3c203ade1833b41(@types/react@18.3.18)(react@18.3.1) mineflayer: specifier: github:zardoy/mineflayer#gen-the-master - version: https://codeload.github.com/zardoy/mineflayer/tar.gz/8aaec9f31657ebb5195cc13db09e5fc6760d4a09(encoding@0.1.13) + version: https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13) mineflayer-mouse: specifier: ^0.1.17 version: 0.1.17 @@ -6649,8 +6649,8 @@ packages: resolution: {tarball: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/89c33d396f3fde4804c71f4be3c203ade1833b41} version: 1.0.1 - minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb: - resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb} + minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074: + resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074} version: 1.60.1 engines: {node: '>=22'} @@ -6669,8 +6669,8 @@ packages: resolution: {integrity: sha512-0eCR8pnGb42Qd9QmAxOjl0PhA5Fa+9+6H1G/YsbsO5rg5mDf94Tusqp/8NAGLPQCPVDzbarLskXdjR3h0E0bEQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/8aaec9f31657ebb5195cc13db09e5fc6760d4a09: - resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/8aaec9f31657ebb5195cc13db09e5fc6760d4a09} + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8: + resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8} version: 8.0.0 engines: {node: '>=22'} @@ -11308,7 +11308,7 @@ snapshots: dependencies: '@nxg-org/mineflayer-util-plugin': 1.8.4 minecraft-data: 3.92.0 - mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/8aaec9f31657ebb5195cc13db09e5fc6760d4a09(encoding@0.1.13) + mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-item: 1.17.0 prismarine-physics: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b @@ -13069,7 +13069,7 @@ snapshots: flatmap: 0.0.3 long: 5.3.1 minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=2a88e61fea1825d9fa5f1584fde810421d553d23e45e6dc829c3697ee3358bea)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=2a88e61fea1825d9fa5f1584fde810421d553d23e45e6dc829c3697ee3358bea)(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 @@ -13105,7 +13105,7 @@ snapshots: flatmap: 0.0.3 long: 5.3.1 minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=2a88e61fea1825d9fa5f1584fde810421d553d23e45e6dc829c3697ee3358bea)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=2a88e61fea1825d9fa5f1584fde810421d553d23e45e6dc829c3697ee3358bea)(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 @@ -16948,12 +16948,12 @@ snapshots: maxrects-packer: '@zardoy/maxrects-packer@2.7.4' zod: 3.24.2 - mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/8aaec9f31657ebb5195cc13db09e5fc6760d4a09(encoding@0.1.13)): + mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13)): dependencies: '@zardoy/flying-squid': 0.0.49(encoding@0.1.13) exit-hook: 2.2.1 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=2a88e61fea1825d9fa5f1584fde810421d553d23e45e6dc829c3697ee3358bea)(encoding@0.1.13) - mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/8aaec9f31657ebb5195cc13db09e5fc6760d4a09(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=2a88e61fea1825d9fa5f1584fde810421d553d23e45e6dc829c3697ee3358bea)(encoding@0.1.13) + mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13) prismarine-item: 1.17.0 ws: 8.18.1 transitivePeerDependencies: @@ -17271,7 +17271,7 @@ snapshots: - '@types/react' - react - minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=2a88e61fea1825d9fa5f1584fde810421d553d23e45e6dc829c3697ee3358bea)(encoding@0.1.13): + minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=2a88e61fea1825d9fa5f1584fde810421d553d23e45e6dc829c3697ee3358bea)(encoding@0.1.13): dependencies: '@types/node-rsa': 1.1.4 '@types/readable-stream': 4.0.18 @@ -17323,7 +17323,7 @@ snapshots: mineflayer-item-map-downloader@https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad)(encoding@0.1.13): dependencies: - mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/8aaec9f31657ebb5195cc13db09e5fc6760d4a09(encoding@0.1.13) + mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13) sharp: 0.30.7 transitivePeerDependencies: - encoding @@ -17338,11 +17338,11 @@ snapshots: transitivePeerDependencies: - supports-color - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/8aaec9f31657ebb5195cc13db09e5fc6760d4a09(encoding@0.1.13): + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13): dependencies: '@nxg-org/mineflayer-physics-util': 1.8.10 minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=2a88e61fea1825d9fa5f1584fde810421d553d23e45e6dc829c3697ee3358bea)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=2a88e61fea1825d9fa5f1584fde810421d553d23e45e6dc829c3697ee3358bea)(encoding@0.1.13) prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chat: 1.11.0 From 4a5f2e799ca1778cd45dce9fa3132d66881bec37 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Wed, 20 Aug 2025 20:02:57 +0300 Subject: [PATCH 143/181] disable experimentalClientSelfReload by default until it's reworked with more fine-tuned checks against server connection --- src/defaultOptions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/defaultOptions.ts b/src/defaultOptions.ts index 28b64059..6045e70b 100644 --- a/src/defaultOptions.ts +++ b/src/defaultOptions.ts @@ -84,7 +84,7 @@ export const defaultOptions = { gameMode: 1 } as any, preferLoadReadonly: false, - experimentalClientSelfReload: true, + experimentalClientSelfReload: false, remoteSoundsSupport: false, remoteSoundsLoadTimeout: 500, disableLoadPrompts: false, From 72e9e656cca70d66372ddfa722abdf01fb4981e3 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Wed, 20 Aug 2025 20:42:40 +0300 Subject: [PATCH 144/181] new! helpful errors on custom channels payloads! --- patches/minecraft-protocol.patch | 13 +++++++++++++ src/mineflayer/mc-protocol.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/patches/minecraft-protocol.patch b/patches/minecraft-protocol.patch index 5dec44d7..e29f87d9 100644 --- a/patches/minecraft-protocol.patch +++ b/patches/minecraft-protocol.patch @@ -73,6 +73,19 @@ index 63cc2bd9615100bd2fd63dfe14c094aa6b8cd1c9..36df57d1196af9761d920fa285ac48f8 } function onJoinServerResponse (err) { +diff --git a/src/client/pluginChannels.js b/src/client/pluginChannels.js +index 671eb452f31e6b5fcd57d715f1009d010160c65f..7f69f511c8fb97d431ec5125c851b49be8e2ab76 100644 +--- a/src/client/pluginChannels.js ++++ b/src/client/pluginChannels.js +@@ -57,7 +57,7 @@ module.exports = function (client, options) { + try { + packet.data = proto.parsePacketBuffer(channel, packet.data).data + } catch (error) { +- client.emit('error', error) ++ client.emit('error', error, { customPayload: packet }) + return + } + } diff --git a/src/client.js b/src/client.js index e369e77d055ba919e8f9da7b8e8b5dc879c74cf4..54bb9e6644388e9b6bd42b3012951875989cdf0c 100644 --- a/src/client.js diff --git a/src/mineflayer/mc-protocol.ts b/src/mineflayer/mc-protocol.ts index a0348c5d..0171387a 100644 --- a/src/mineflayer/mc-protocol.ts +++ b/src/mineflayer/mc-protocol.ts @@ -11,6 +11,36 @@ import { getWebsocketStream } from './websocket-core' let lastPacketTime = 0 customEvents.on('mineflayerBotCreated', () => { + // const oldParsePacketBuffer = bot._client.deserializer.parsePacketBuffer + // try { + // const parsed = oldParsePacketBuffer(buffer) + // } catch (err) { + // debugger + // reportError(new Error(`Error parsing packet ${buffer.subarray(0, 30).toString('hex')}`, { cause: err })) + // throw err + // } + // } + class MinecraftProtocolError extends Error { + constructor (message: string, cause?: Error, public data?: any) { + if (data?.customPayload) { + message += ` (Custom payload: ${data.customPayload.channel})` + } + super(message, { cause }) + this.name = 'MinecraftProtocolError' + } + } + + const onClientError = (err, data) => { + const error = new MinecraftProtocolError(`Minecraft protocol client error: ${err.message}`, err, data) + reportError(error) + } + if (typeof bot._client['_events'].error === 'function') { + // dont report to bot for more explicit error + bot._client['_events'].error = onClientError + } else { + bot._client.on('error' as any, onClientError) + } + // todo move more code here if (!appQueryParams.noPacketsValidation) { (bot._client as unknown as Client).on('packet', (data, packetMeta, buffer, fullBuffer) => { From 6e0d54ea17178a091d8c103a48f82a4723f5b23d Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Wed, 20 Aug 2025 20:45:02 +0300 Subject: [PATCH 145/181] up mc-protocol patch --- pnpm-lock.yaml | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e7cb1872..c7e1d1b9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,7 +23,7 @@ overrides: patchedDependencies: minecraft-protocol: - hash: 2a88e61fea1825d9fa5f1584fde810421d553d23e45e6dc829c3697ee3358bea + hash: 4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b path: patches/minecraft-protocol.patch mineflayer-item-map-downloader@1.2.0: hash: a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad @@ -143,7 +143,7 @@ importers: version: 3.92.0 minecraft-protocol: specifier: github:PrismarineJS/node-minecraft-protocol#master - version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=2a88e61fea1825d9fa5f1584fde810421d553d23e45e6dc829c3697ee3358bea)(encoding@0.1.13) + version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13) mineflayer-item-map-downloader: specifier: github:zardoy/mineflayer-item-map-downloader version: https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad)(encoding@0.1.13) @@ -430,13 +430,13 @@ importers: version: 1.3.9 prismarine-block: specifier: github:zardoy/prismarine-block#next-era - version: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 + version: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0) prismarine-chunk: specifier: github:zardoy/prismarine-chunk#master version: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0) prismarine-schematic: specifier: ^1.2.0 - version: 1.2.3 + version: 1.2.3(prismarine-registry@1.11.0) process: specifier: ^0.11.10 version: 0.11.10 @@ -6651,7 +6651,7 @@ packages: minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074: resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074} - version: 1.60.1 + version: 1.61.0 engines: {node: '>=22'} minecraft-wrap@1.6.0: @@ -11309,7 +11309,7 @@ snapshots: '@nxg-org/mineflayer-util-plugin': 1.8.4 minecraft-data: 3.92.0 mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13) - prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 + prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0) prismarine-item: 1.17.0 prismarine-physics: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b vec3: 0.1.10 @@ -13069,7 +13069,7 @@ snapshots: flatmap: 0.0.3 long: 5.3.1 minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=2a88e61fea1825d9fa5f1584fde810421d553d23e45e6dc829c3697ee3358bea)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 @@ -13105,7 +13105,7 @@ snapshots: flatmap: 0.0.3 long: 5.3.1 minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=2a88e61fea1825d9fa5f1584fde810421d553d23e45e6dc829c3697ee3358bea)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 @@ -16952,7 +16952,7 @@ snapshots: dependencies: '@zardoy/flying-squid': 0.0.49(encoding@0.1.13) exit-hook: 2.2.1 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=2a88e61fea1825d9fa5f1584fde810421d553d23e45e6dc829c3697ee3358bea)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13) mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13) prismarine-item: 1.17.0 ws: 8.18.1 @@ -17271,7 +17271,7 @@ snapshots: - '@types/react' - react - minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=2a88e61fea1825d9fa5f1584fde810421d553d23e45e6dc829c3697ee3358bea)(encoding@0.1.13): + minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13): dependencies: '@types/node-rsa': 1.1.4 '@types/readable-stream': 4.0.18 @@ -17342,9 +17342,9 @@ snapshots: dependencies: '@nxg-org/mineflayer-physics-util': 1.8.10 minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=2a88e61fea1825d9fa5f1584fde810421d553d23e45e6dc829c3697ee3358bea)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13) prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) - prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 + prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0) prismarine-chat: 1.11.0 prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0) prismarine-entity: 2.5.0 @@ -18135,7 +18135,7 @@ snapshots: minecraft-data: 3.92.0 prismarine-registry: 1.11.0 - prismarine-block@https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9: + prismarine-block@https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0): dependencies: minecraft-data: 3.92.0 prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) @@ -18143,6 +18143,8 @@ snapshots: prismarine-item: 1.17.0 prismarine-nbt: 2.7.0 prismarine-registry: 1.11.0 + transitivePeerDependencies: + - prismarine-registry prismarine-chat@1.11.0: dependencies: @@ -18153,7 +18155,7 @@ snapshots: prismarine-chunk@https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0): dependencies: prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) - prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 + prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0) prismarine-nbt: 2.7.0 prismarine-registry: 1.11.0 smart-buffer: 4.2.0 @@ -18187,7 +18189,7 @@ snapshots: prismarine-provider-anvil@https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.92.0): dependencies: - prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 + prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0) prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0) prismarine-nbt: 2.7.0 prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c @@ -18211,16 +18213,18 @@ snapshots: prismarine-registry@1.11.0: dependencies: minecraft-data: 3.92.0 - prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 + prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0) prismarine-nbt: 2.7.0 - prismarine-schematic@1.2.3: + prismarine-schematic@1.2.3(prismarine-registry@1.11.0): dependencies: minecraft-data: 3.92.0 - prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 + prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0) prismarine-nbt: 2.7.0 prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c vec3: 0.1.10 + transitivePeerDependencies: + - prismarine-registry prismarine-windows@2.9.0: dependencies: From a12c61bc6c8f3ede1910dba4567d71d7f907feeb Mon Sep 17 00:00:00 2001 From: Vitaly Date: Thu, 21 Aug 2025 13:21:02 +0300 Subject: [PATCH 146/181] add simple monaco (#418) --- package.json | 1 + pnpm-lock.yaml | 58 +++++++++++++++----- src/core/ideChannels.ts | 106 +++++++++++++++++++++++++++++++++++++ src/customChannels.ts | 2 + src/react/MonacoEditor.css | 58 ++++++++++++++++++++ src/react/MonacoEditor.tsx | 73 +++++++++++++++++++++++++ src/reactUi.tsx | 3 +- 7 files changed, 286 insertions(+), 15 deletions(-) create mode 100644 src/core/ideChannels.ts create mode 100644 src/react/MonacoEditor.css create mode 100644 src/react/MonacoEditor.tsx diff --git a/package.json b/package.json index 1d9dfc0c..a8c2c4e7 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "dependencies": { "@dimaka/interface": "0.0.3-alpha.0", "@floating-ui/react": "^0.26.1", + "@monaco-editor/react": "^4.7.0", "@nxg-org/mineflayer-auto-jump": "^0.7.18", "@nxg-org/mineflayer-tracker": "1.3.0", "@react-oauth/google": "^0.12.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c7e1d1b9..8acb9681 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,6 +42,9 @@ importers: '@floating-ui/react': specifier: ^0.26.1 version: 0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@monaco-editor/react': + specifier: ^4.7.0 + version: 4.7.0(monaco-editor@0.52.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@nxg-org/mineflayer-auto-jump': specifier: ^0.7.18 version: 0.7.18 @@ -430,13 +433,13 @@ importers: version: 1.3.9 prismarine-block: specifier: github:zardoy/prismarine-block#next-era - version: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0) + version: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chunk: specifier: github:zardoy/prismarine-chunk#master version: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0) prismarine-schematic: specifier: ^1.2.0 - version: 1.2.3(prismarine-registry@1.11.0) + version: 1.2.3 process: specifier: ^0.11.10 version: 0.11.10 @@ -1989,6 +1992,16 @@ packages: '@module-federation/webpack-bundler-runtime@0.11.2': resolution: {integrity: sha512-WdwIE6QF+MKs/PdVu0cKPETF743JB9PZ62/qf7Uo3gU4fjsUMc37RnbJZ/qB60EaHHfjwp1v6NnhZw1r4eVsnw==} + '@monaco-editor/loader@1.5.0': + resolution: {integrity: sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==} + + '@monaco-editor/react@4.7.0': + resolution: {integrity: sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==} + peerDependencies: + monaco-editor: '>= 0.25.0 < 1' + react: ^18.2.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@msgpack/msgpack@2.8.0': resolution: {integrity: sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==} engines: {node: '>= 10'} @@ -6769,6 +6782,9 @@ packages: mojangson@2.0.4: resolution: {integrity: sha512-HYmhgDjr1gzF7trGgvcC/huIg2L8FsVbi/KacRe6r1AswbboGVZDS47SOZlomPuMWvZLas8m9vuHHucdZMwTmQ==} + monaco-editor@0.52.2: + resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==} + moo@0.5.2: resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==} @@ -8373,6 +8389,9 @@ packages: stacktrace-js@2.0.2: resolution: {integrity: sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==} + state-local@1.0.7: + resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + static-extend@0.1.2: resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==} engines: {node: '>=0.10.0'} @@ -11254,6 +11273,17 @@ snapshots: '@module-federation/runtime': 0.11.2 '@module-federation/sdk': 0.11.2 + '@monaco-editor/loader@1.5.0': + dependencies: + state-local: 1.0.7 + + '@monaco-editor/react@4.7.0(monaco-editor@0.52.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@monaco-editor/loader': 1.5.0 + monaco-editor: 0.52.2 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@msgpack/msgpack@2.8.0': {} '@ndelangen/get-tarball@3.0.9': @@ -11309,7 +11339,7 @@ snapshots: '@nxg-org/mineflayer-util-plugin': 1.8.4 minecraft-data: 3.92.0 mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13) - prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0) + prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-item: 1.17.0 prismarine-physics: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b vec3: 0.1.10 @@ -17344,7 +17374,7 @@ snapshots: minecraft-data: 3.92.0 minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13) prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) - prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0) + prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chat: 1.11.0 prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0) prismarine-entity: 2.5.0 @@ -17461,6 +17491,8 @@ snapshots: dependencies: nearley: 2.20.1 + monaco-editor@0.52.2: {} + moo@0.5.2: {} morgan@1.10.0: @@ -18135,7 +18167,7 @@ snapshots: minecraft-data: 3.92.0 prismarine-registry: 1.11.0 - prismarine-block@https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0): + prismarine-block@https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9: dependencies: minecraft-data: 3.92.0 prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) @@ -18143,8 +18175,6 @@ snapshots: prismarine-item: 1.17.0 prismarine-nbt: 2.7.0 prismarine-registry: 1.11.0 - transitivePeerDependencies: - - prismarine-registry prismarine-chat@1.11.0: dependencies: @@ -18155,7 +18185,7 @@ snapshots: prismarine-chunk@https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0): dependencies: prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) - prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0) + prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-nbt: 2.7.0 prismarine-registry: 1.11.0 smart-buffer: 4.2.0 @@ -18189,7 +18219,7 @@ snapshots: prismarine-provider-anvil@https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.92.0): dependencies: - prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0) + prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0) prismarine-nbt: 2.7.0 prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c @@ -18213,18 +18243,16 @@ snapshots: prismarine-registry@1.11.0: dependencies: minecraft-data: 3.92.0 - prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0) + prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-nbt: 2.7.0 - prismarine-schematic@1.2.3(prismarine-registry@1.11.0): + prismarine-schematic@1.2.3: dependencies: minecraft-data: 3.92.0 - prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0) + prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-nbt: 2.7.0 prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c vec3: 0.1.10 - transitivePeerDependencies: - - prismarine-registry prismarine-windows@2.9.0: dependencies: @@ -19521,6 +19549,8 @@ snapshots: stack-generator: 2.0.10 stacktrace-gps: 3.1.2 + state-local@1.0.7: {} + static-extend@0.1.2: dependencies: define-property: 0.2.5 diff --git a/src/core/ideChannels.ts b/src/core/ideChannels.ts new file mode 100644 index 00000000..a9c517f7 --- /dev/null +++ b/src/core/ideChannels.ts @@ -0,0 +1,106 @@ +import { proxy } from 'valtio' + +export const ideState = proxy({ + id: '', + contents: '', + line: 0, + column: 0, + language: 'typescript', + title: '', +}) +globalThis.ideState = ideState + +export const registerIdeChannels = () => { + registerIdeOpenChannel() + registerIdeSaveChannel() +} + +const registerIdeOpenChannel = () => { + const CHANNEL_NAME = 'minecraft-web-client:ide-open' + + const packetStructure = [ + 'container', + [ + { + name: 'id', + type: ['pstring', { countType: 'i16' }] + }, + { + name: 'language', + type: ['pstring', { countType: 'i16' }] + }, + { + name: 'contents', + type: ['pstring', { countType: 'i16' }] + }, + { + name: 'line', + type: 'i32' + }, + { + name: 'column', + type: 'i32' + }, + { + name: 'title', + type: ['pstring', { countType: 'i16' }] + } + ] + ] + + bot._client.registerChannel(CHANNEL_NAME, packetStructure, true) + + bot._client.on(CHANNEL_NAME as any, (data) => { + const { id, language, contents, line, column, title } = data + + ideState.contents = contents + ideState.line = line + ideState.column = column + ideState.id = id + ideState.language = language || 'typescript' + ideState.title = title + }) + + console.debug(`registered custom channel ${CHANNEL_NAME} channel`) +} +const IDE_SAVE_CHANNEL_NAME = 'minecraft-web-client:ide-save' +const registerIdeSaveChannel = () => { + + const packetStructure = [ + 'container', + [ + { + name: 'id', + type: ['pstring', { countType: 'i16' }] + }, + { + name: 'contents', + type: ['pstring', { countType: 'i16' }] + }, + { + name: 'language', + type: ['pstring', { countType: 'i16' }] + }, + { + name: 'line', + type: 'i32' + }, + { + name: 'column', + type: 'i32' + }, + ] + ] + bot._client.registerChannel(IDE_SAVE_CHANNEL_NAME, packetStructure, true) +} + +export const saveIde = () => { + bot._client.writeChannel(IDE_SAVE_CHANNEL_NAME, { + id: ideState.id, + contents: ideState.contents, + language: ideState.language, + // todo: reflect updated + line: ideState.line, + column: ideState.column, + }) +} diff --git a/src/customChannels.ts b/src/customChannels.ts index 6d3aa7e9..717c7c93 100644 --- a/src/customChannels.ts +++ b/src/customChannels.ts @@ -2,6 +2,7 @@ import PItem from 'prismarine-item' import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods' import { options } from './optionsStorage' import { jeiCustomCategories } from './inventoryWindows' +import { registerIdeChannels } from './core/ideChannels' export default () => { customEvents.on('mineflayerBotCreated', async () => { @@ -17,6 +18,7 @@ export default () => { registeredJeiChannel() registerBlockInteractionsCustomizationChannel() registerWaypointChannels() + registerIdeChannels() }) } diff --git a/src/react/MonacoEditor.css b/src/react/MonacoEditor.css new file mode 100644 index 00000000..86d2ad0a --- /dev/null +++ b/src/react/MonacoEditor.css @@ -0,0 +1,58 @@ +.monaco-editor-container { + position: fixed; + inset: 0; + z-index: 1000; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 16px; + background-color: rgba(0, 0, 0, 0.5); +} + +.monaco-editor-title { + font-size: 20px; + font-weight: bold; + color: #fff; + margin-bottom: 8px; +} + +.monaco-editor-wrapper { + position: relative; + width: 100%; + height: 100%; + max-width: 80vw; + max-height: 80vh; + border: 3px solid #000; + background-color: #000; + padding: 3px; + box-shadow: inset 0 0 0 1px #fff, inset 0 0 0 2px #000; +} + +.monaco-editor-close { + position: fixed; + top: 16px; + left: 16px; + z-index: 1001; + cursor: pointer; + padding: 8px; +} + +@media (max-width: 768px) { + .monaco-editor-container { + padding: 0; + } + .monaco-editor-wrapper { + max-width: 100%; + max-height: 100%; + border-radius: 0; + } + .monaco-editor-close { + top: 8px; + left: 8px; + } + .monaco-editor-title { + /* todo: make it work on mobile */ + display: none; + } +} diff --git a/src/react/MonacoEditor.tsx b/src/react/MonacoEditor.tsx new file mode 100644 index 00000000..32162b21 --- /dev/null +++ b/src/react/MonacoEditor.tsx @@ -0,0 +1,73 @@ +import { proxy, useSnapshot } from 'valtio' +import { useEffect } from 'react' +import { Editor } from '@monaco-editor/react' +import PixelartIcon, { pixelartIcons } from '../react/PixelartIcon' +import { useIsModalActive } from '../react/utilsApp' +import { showNotification } from '../react/NotificationProvider' +import { hideModal, showModal } from '../globalState' +import { ideState, saveIde } from '../core/ideChannels' +import './MonacoEditor.css' + +export default () => { + const { contents, line, column, id, language, title } = useSnapshot(ideState) + const isModalActive = useIsModalActive('monaco-editor') + const bodyFont = getComputedStyle(document.body).fontFamily + + useEffect(() => { + if (id && !isModalActive) { + showModal({ reactType: 'monaco-editor' }) + } + if (!id && isModalActive) { + hideModal() + } + }, [id]) + + useEffect(() => { + if (!isModalActive && id) { + try { + saveIde() + } catch (err) { + reportError(err) + showNotification('Failed to save the editor', 'Please try again', true) + } + ideState.id = '' + ideState.contents = '' + } + }, [isModalActive]) + + if (!isModalActive) return null + + return
    +
    + { + hideModal() + }} + /> +
    +
    + {title} +
    +
    + { + ideState.contents = value ?? '' + }} + value={contents} + options={{ + fontFamily: bodyFont, + minimap: { + enabled: true, + }, + }} + /> +
    +
    +} diff --git a/src/reactUi.tsx b/src/reactUi.tsx index 4f8c4541..1e6f13eb 100644 --- a/src/reactUi.tsx +++ b/src/reactUi.tsx @@ -67,6 +67,7 @@ import GlobalOverlayHints from './react/GlobalOverlayHints' import FullscreenTime from './react/FullscreenTime' import StorageConflictModal from './react/StorageConflictModal' import FireRenderer from './react/FireRenderer' +import MonacoEditor from './react/MonacoEditor' const isFirefox = ua.getBrowser().name === 'Firefox' if (isFirefox) { @@ -248,7 +249,6 @@ const App = () => { - @@ -259,6 +259,7 @@ const App = () => {
    + From bc2972fe99692510643cb482128ab978970a9c2f Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sun, 24 Aug 2025 19:53:44 +0300 Subject: [PATCH 147/181] fix registering custom channels too late (a few ms diff) --- src/customChannels.ts | 19 ++++++++----------- src/resourcePack.ts | 11 ----------- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/src/customChannels.ts b/src/customChannels.ts index 717c7c93..8e70078f 100644 --- a/src/customChannels.ts +++ b/src/customChannels.ts @@ -7,18 +7,15 @@ import { registerIdeChannels } from './core/ideChannels' export default () => { customEvents.on('mineflayerBotCreated', async () => { if (!options.customChannels) return - await new Promise(resolve => { - bot.once('login', () => { - resolve(true) - }) + bot.once('login', () => { + registerBlockModelsChannel() + registerMediaChannels() + registerSectionAnimationChannels() + registeredJeiChannel() + registerBlockInteractionsCustomizationChannel() + registerWaypointChannels() + registerIdeChannels() }) - registerBlockModelsChannel() - registerMediaChannels() - registerSectionAnimationChannels() - registeredJeiChannel() - registerBlockInteractionsCustomizationChannel() - registerWaypointChannels() - registerIdeChannels() }) } diff --git a/src/resourcePack.ts b/src/resourcePack.ts index e0f0fca4..ea6c73fd 100644 --- a/src/resourcePack.ts +++ b/src/resourcePack.ts @@ -486,17 +486,6 @@ const downloadAndUseResourcePack = async (url: string, progressReporter: Progres } } -const waitForGameEvent = async () => { - if (miscUiState.gameLoaded) return - await new Promise(resolve => { - const listener = () => resolve() - customEvents.once('gameLoaded', listener) - watchUnloadForCleanup(() => { - customEvents.removeListener('gameLoaded', listener) - }) - }) -} - export const onAppLoad = () => { customEvents.on('mineflayerBotCreated', () => { // todo also handle resourcePack From 8f62fbd4daddbdccf61191949889925bed2b4632 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Tue, 26 Aug 2025 13:50:36 +0300 Subject: [PATCH 148/181] fix: window title sometimes was not showing up on old versions --- src/chatUtils.ts | 8 ++++++++ src/inventoryWindows.ts | 15 +++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/chatUtils.ts b/src/chatUtils.ts index 88437bc3..849d5847 100644 --- a/src/chatUtils.ts +++ b/src/chatUtils.ts @@ -118,6 +118,14 @@ export const formatMessage = (message: MessageInput, mcData: IndexedData = globa return msglist } +export const messageToString = (message: MessageInput | string) => { + if (typeof message === 'string') { + return message + } + const msglist = formatMessage(message) + return msglist.map(msg => msg.text).join('') +} + const blockToItemRemaps = { water: 'water_bucket', lava: 'lava_bucket', diff --git a/src/inventoryWindows.ts b/src/inventoryWindows.ts index a9f89d1b..166e42a7 100644 --- a/src/inventoryWindows.ts +++ b/src/inventoryWindows.ts @@ -57,12 +57,23 @@ export const onGameLoad = () => { return type } + const maybeParseNbtJson = (data: any) => { + if (typeof data === 'string') { + try { + data = JSON.parse(data) + } catch (err) { + // ignore + } + } + return nbt.simplify(data) ?? data + } + bot.on('windowOpen', (win) => { const implementedWindow = implementedContainersGuiMap[mapWindowType(win.type as string, win.inventoryStart)] if (implementedWindow) { - openWindow(implementedWindow, nbt.simplify(win.title as any)) + openWindow(implementedWindow, maybeParseNbtJson(win.title)) } else if (options.unimplementedContainers) { - openWindow('ChestWin', nbt.simplify(win.title as any)) + openWindow('ChestWin', maybeParseNbtJson(win.title)) } else { // todo format displayClientChat(`[client error] cannot open unimplemented window ${win.id} (${win.type}). Slots: ${win.slots.map(item => getItemName(item)).filter(Boolean).join(', ')}`) From 9718610131a44b1d0aa46fdf97d696d0af4dc663 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Wed, 27 Aug 2025 12:08:20 +0300 Subject: [PATCH 149/181] ci: add deployment step for mcw-mcraft-page repository in GitHub Actions --- .github/workflows/release.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cbf52251..68ead92f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,6 +49,18 @@ jobs: publish_dir: .vercel/output/static force_orphan: true + # Deploy to mcw-mcraft-page repository (github.mcraft.fun) + - name: Deploy to mcw-mcraft-page repository + uses: peaceiris/actions-gh-pages@v3 + with: + personal_token: ${{ secrets.MCW_MCRAFT_PAGE_DEPLOY_TOKEN }} + external_repository: ${{ github.repository_owner }}/mcw-mcraft-pages + publish_dir: .vercel/output/static + publish_branch: main + destination_dir: docs + cname: github.mcraft.fun + force_orphan: true + - name: Change index.html title run: | # change Minecraft Web Client to Minecraft Web Client — Free Online Browser Version From 2a1746eb7a6bd96f40de9cffbb13d78e422388f2 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Wed, 27 Aug 2025 13:33:39 +0300 Subject: [PATCH 150/181] [skip ci] fix repository name --- .github/workflows/release.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 68ead92f..3710eebd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,12 +49,11 @@ jobs: publish_dir: .vercel/output/static force_orphan: true - # Deploy to mcw-mcraft-page repository (github.mcraft.fun) - - name: Deploy to mcw-mcraft-page repository + - name: Deploy to mwc-mcraft-pages repository uses: peaceiris/actions-gh-pages@v3 with: personal_token: ${{ secrets.MCW_MCRAFT_PAGE_DEPLOY_TOKEN }} - external_repository: ${{ github.repository_owner }}/mcw-mcraft-pages + external_repository: ${{ github.repository_owner }}/mwc-mcraft-pages publish_dir: .vercel/output/static publish_branch: main destination_dir: docs From 1f240d8c2047de469965076dc23ae72fe534a83a Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Wed, 27 Aug 2025 19:50:53 +0300 Subject: [PATCH 151/181] up mouse allowing to disable positive break block --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- src/customChannels.ts | 14 +------------- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index a8c2c4e7..5dcd9547 100644 --- a/package.json +++ b/package.json @@ -157,7 +157,7 @@ "mc-assets": "^0.2.62", "minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next", "mineflayer": "github:zardoy/mineflayer#gen-the-master", - "mineflayer-mouse": "^0.1.17", + "mineflayer-mouse": "^0.1.21", "npm-run-all": "^4.1.5", "os-browserify": "^0.3.0", "path-browserify": "^1.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8acb9681..6c393ac7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -347,8 +347,8 @@ importers: specifier: github:zardoy/mineflayer#gen-the-master version: https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13) mineflayer-mouse: - specifier: ^0.1.17 - version: 0.1.17 + specifier: ^0.1.21 + version: 0.1.21 npm-run-all: specifier: ^4.1.5 version: 4.1.5 @@ -6678,8 +6678,8 @@ packages: resolution: {tarball: https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824} version: 1.2.0 - mineflayer-mouse@0.1.17: - resolution: {integrity: sha512-0eCR8pnGb42Qd9QmAxOjl0PhA5Fa+9+6H1G/YsbsO5rg5mDf94Tusqp/8NAGLPQCPVDzbarLskXdjR3h0E0bEQ==} + mineflayer-mouse@0.1.21: + resolution: {integrity: sha512-1XTVuw3twIrEcqQ1QRSB8NcStIUEZ+tbxiAG6rOrN/9M4thhtlS5PTJzFdmdrcYgWEBLvuOdJszaKE5zFfiXhg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8: @@ -17359,7 +17359,7 @@ snapshots: - encoding - supports-color - mineflayer-mouse@0.1.17: + mineflayer-mouse@0.1.21: dependencies: change-case: 5.4.4 debug: 4.4.1 diff --git a/src/customChannels.ts b/src/customChannels.ts index 8e70078f..b566f9dd 100644 --- a/src/customChannels.ts +++ b/src/customChannels.ts @@ -47,19 +47,7 @@ const registerBlockInteractionsCustomizationChannel = () => { registerChannel(CHANNEL_NAME, packetStructure, (data) => { const config = JSON.parse(data.newConfiguration) - if (config.customBreakTime !== undefined && Object.values(config.customBreakTime).every(x => typeof x === 'number')) { - bot.mouse.customBreakTime = config.customBreakTime - } - if (config.customBreakTimeToolAllowance !== undefined) { - bot.mouse.customBreakTimeToolAllowance = new Set(config.customBreakTimeToolAllowance) - } - - if (config.blockPlacePrediction !== undefined) { - bot.mouse.settings.blockPlacePrediction = config.blockPlacePrediction - } - if (config.blockPlacePredictionDelay !== undefined) { - bot.mouse.settings.blockPlacePredictionDelay = config.blockPlacePredictionDelay - } + bot.mouse.setConfigFromPacket(config) }, true) } From e81d608554d8d0e98982df7f3570b70793a0927c Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Wed, 27 Aug 2025 19:52:09 +0300 Subject: [PATCH 152/181] fix cname --- .github/workflows/release.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3710eebd..3e8c4136 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,6 +49,10 @@ jobs: publish_dir: .vercel/output/static force_orphan: true + # Create CNAME file for custom domain + - name: Create CNAME file + run: echo "github.mcraft.fun" > .vercel/output/static/CNAME + - name: Deploy to mwc-mcraft-pages repository uses: peaceiris/actions-gh-pages@v3 with: @@ -57,7 +61,6 @@ jobs: publish_dir: .vercel/output/static publish_branch: main destination_dir: docs - cname: github.mcraft.fun force_orphan: true - name: Change index.html title From d0d5234ba43c33d11325696a763163196c3206ad Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sun, 31 Aug 2025 18:31:49 +0300 Subject: [PATCH 153/181] fix: stop right click emulation once window got opened eg chest --- src/mineflayer/plugins/mouse.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mineflayer/plugins/mouse.ts b/src/mineflayer/plugins/mouse.ts index fc1ce0fd..14e19345 100644 --- a/src/mineflayer/plugins/mouse.ts +++ b/src/mineflayer/plugins/mouse.ts @@ -110,7 +110,7 @@ const domListeners = (bot: Bot) => { }, { signal: abortController.signal }) bot.mouse.beforeUpdateChecks = () => { - if (!document.hasFocus()) { + if (!document.hasFocus() || !isGameActive(true)) { // deactive all buttons bot.mouse.buttons.fill(false) } From cb82188272ef5bec257efdf0150460136cf267bd Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sun, 31 Aug 2025 19:31:26 +0300 Subject: [PATCH 154/181] add addPing query param for testing --- README.MD | 1 + src/appParams.ts | 1 + src/index.ts | 2 +- src/mineflayer/mc-protocol.ts | 3 ++- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.MD b/README.MD index 7978cee5..82aac011 100644 --- a/README.MD +++ b/README.MD @@ -176,6 +176,7 @@ Server specific: - `?lockConnect=true` - Only works then `ip` parameter is set. Disables cancel/save buttons and all inputs in the connect screen already set as parameters. Useful for integrates iframes. - `?autoConnect=true` - Only works then `ip` and `version` parameters are set and `allowAutoConnect` is `true` in config.json! Directly connects to the specified server. Useful for integrates iframes. - `?serversList=` - `` can be a list of servers in the format `ip:version,ip` or a url to a json file with the same format (array) or a txt file with line-delimited list of server IPs. +- `?addPing=` - Add a latency to both sides of the connection. Useful for testing ping issues. For example `?addPing=100` will add 200ms to your ping. Single player specific: diff --git a/src/appParams.ts b/src/appParams.ts index 8d487f8d..4c8ca186 100644 --- a/src/appParams.ts +++ b/src/appParams.ts @@ -47,6 +47,7 @@ export type AppQsParams = { connectText?: string freezeSettings?: string testIosCrash?: string + addPing?: string // Replay params replayFilter?: string diff --git a/src/index.ts b/src/index.ts index 4a118cee..54731a16 100644 --- a/src/index.ts +++ b/src/index.ts @@ -305,7 +305,7 @@ export async function connect (connectOptions: ConnectOptions) { if (connectOptions.server && !connectOptions.viewerWsConnect && !parsedServer.isWebSocket) { console.log(`using proxy ${proxy.host}:${proxy.port || location.port}`) - net['setProxy']({ hostname: proxy.host, port: proxy.port, headers: { Authorization: `Bearer ${new URLSearchParams(location.search).get('token') ?? ''}` } }) + net['setProxy']({ hostname: proxy.host, port: proxy.port, headers: { Authorization: `Bearer ${new URLSearchParams(location.search).get('token') ?? ''}` }, artificialDelay: appQueryParams.addPing ? Number(appQueryParams.addPing) : undefined }) } const renderDistance = singleplayer ? renderDistanceSingleplayer : multiplayerRenderDistance diff --git a/src/mineflayer/mc-protocol.ts b/src/mineflayer/mc-protocol.ts index 0171387a..cd21d01f 100644 --- a/src/mineflayer/mc-protocol.ts +++ b/src/mineflayer/mc-protocol.ts @@ -130,7 +130,8 @@ export const setProxy = (proxyParams: ProxyParams) => { net['setProxy']({ hostname: proxy.host, port: proxy.port, - headers: proxyParams.headers + headers: proxyParams.headers, + artificialDelay: appQueryParams.addPing ? Number(appQueryParams.addPing) : undefined }) return { proxy From 513201be87401c53231103da9264693a3ccadb92 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Mon, 1 Sep 2025 08:56:08 +0300 Subject: [PATCH 155/181] up browserify --- pnpm-lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c393ac7..6a4773d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -155,7 +155,7 @@ importers: version: 2.0.4 net-browserify: specifier: github:zardoy/prismarinejs-net-browserify - version: https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/17fb901e8ea480a52c8fd46373695be172be8aa5 + version: https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/e754999ffdea67853bc9b10553b5e9908b40f618 node-gzip: specifier: ^1.1.2 version: 1.1.2 @@ -6849,8 +6849,8 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - net-browserify@https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/17fb901e8ea480a52c8fd46373695be172be8aa5: - resolution: {tarball: https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/17fb901e8ea480a52c8fd46373695be172be8aa5} + net-browserify@https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/e754999ffdea67853bc9b10553b5e9908b40f618: + resolution: {tarball: https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/e754999ffdea67853bc9b10553b5e9908b40f618} version: 0.2.4 nice-try@1.0.5: @@ -17574,7 +17574,7 @@ snapshots: neo-async@2.6.2: {} - net-browserify@https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/17fb901e8ea480a52c8fd46373695be172be8aa5: + net-browserify@https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/e754999ffdea67853bc9b10553b5e9908b40f618: dependencies: body-parser: 1.20.3 express: 4.21.2 From 7e3ba8bece2e4c85bcc8ef7f2caa22627f88d50d Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Tue, 2 Sep 2025 19:02:30 +0300 Subject: [PATCH 156/181] up integrated server for the latest fixes and better stability --- package.json | 2 +- pnpm-lock.yaml | 28 ++++++++++++++++++++-------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 5dcd9547..b5f66bfb 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "esbuild-plugin-polyfill-node": "^0.3.0", "express": "^4.18.2", "filesize": "^10.0.12", - "flying-squid": "npm:@zardoy/flying-squid@^0.0.62", + "flying-squid": "npm:@zardoy/flying-squid@^0.0.104", "framer-motion": "^12.9.2", "fs-extra": "^11.1.1", "google-drive-browserfs": "github:zardoy/browserfs#google-drive", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a4773d2..f6188b6d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -121,8 +121,8 @@ importers: specifier: ^10.0.12 version: 10.1.6 flying-squid: - specifier: npm:@zardoy/flying-squid@^0.0.62 - version: '@zardoy/flying-squid@0.0.62(encoding@0.1.13)' + specifier: npm:@zardoy/flying-squid@^0.0.104 + version: '@zardoy/flying-squid@0.0.104(encoding@0.1.13)' framer-motion: specifier: ^12.9.2 version: 12.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -3387,13 +3387,13 @@ 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.49': - resolution: {integrity: sha512-Kt4wr5/R+44tcLU9gjuNG2an9weWeKEpIoKXfsgJN2GGQqdnbd5nBpxfGDdgZ9aMdFugsVW8BsyPZNhj9vbMXA==} + '@zardoy/flying-squid@0.0.104': + resolution: {integrity: sha512-jGhQ7fn7o8UN+mUwZbt9674D37YLuBi+Au4TwKcopCA6huIQdHTFNl2e+0ZSTI5mnhN+NpyVoR3vmtH6L58vHQ==} engines: {node: '>=8'} hasBin: true - '@zardoy/flying-squid@0.0.62': - resolution: {integrity: sha512-M6icydO/yrmwevBhmgKcqEPC63AhWfU/Es9N/uadVrmKaxGm2FQMMLcybbutRYm1xZ6qsdxDUOUZnN56PsVwfQ==} + '@zardoy/flying-squid@0.0.49': + resolution: {integrity: sha512-Kt4wr5/R+44tcLU9gjuNG2an9weWeKEpIoKXfsgJN2GGQqdnbd5nBpxfGDdgZ9aMdFugsVW8BsyPZNhj9vbMXA==} engines: {node: '>=8'} hasBin: true @@ -6444,6 +6444,12 @@ packages: resolution: {integrity: sha512-RYZeD1+joNlPuUpi+tIWkbP0ieVJr+R6IFkI6/8juhSxx9zE4osoSmteybrfspGm8A6u+YbbY1epqRKEMwVR6Q==} engines: {node: '>=18.0.0'} + mc-bridge@0.1.3: + resolution: {integrity: sha512-H9jPt2xEU77itC27dSz3qazHYqN9qVsv4HgMPozg7RqQ1uwgXmEa+ojKIlDtXf/TLJsG6Kv4EbzGa8a1Wh72uA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + minecraft-data: 3.92.0 + mcraft-fun-mineflayer@0.1.23: resolution: {integrity: sha512-qmI1rQQ0Ro5zJdi99rClWLF+mS9JZffgNX2vyWWesS3Hsk3Xblp/8swYTJKHSaFpNgzkVfXV92fEIrBqeH6iKA==} version: 0.1.23 @@ -13088,7 +13094,7 @@ snapshots: '@types/emscripten': 1.40.0 tslib: 1.14.1 - '@zardoy/flying-squid@0.0.49(encoding@0.1.13)': + '@zardoy/flying-squid@0.0.104(encoding@0.1.13)': dependencies: '@tootallnate/once': 2.0.0 chalk: 5.4.1 @@ -13098,11 +13104,13 @@ snapshots: exit-hook: 2.2.1 flatmap: 0.0.3 long: 5.3.1 + mc-bridge: 0.1.3(minecraft-data@3.92.0) minecraft-data: 3.92.0 minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 + prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0) prismarine-entity: 2.5.0 prismarine-item: 1.17.0 @@ -13124,7 +13132,7 @@ snapshots: - encoding - supports-color - '@zardoy/flying-squid@0.0.62(encoding@0.1.13)': + '@zardoy/flying-squid@0.0.49(encoding@0.1.13)': dependencies: '@tootallnate/once': 2.0.0 chalk: 5.4.1 @@ -16978,6 +16986,10 @@ snapshots: maxrects-packer: '@zardoy/maxrects-packer@2.7.4' zod: 3.24.2 + mc-bridge@0.1.3(minecraft-data@3.92.0): + dependencies: + minecraft-data: 3.92.0 + mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13)): dependencies: '@zardoy/flying-squid': 0.0.49(encoding@0.1.13) From 9d54c70fb724884cd98782cdca093aef622095ba Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Tue, 2 Sep 2025 19:05:18 +0300 Subject: [PATCH 157/181] use node 22 --- .github/workflows/benchmark.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index f913b9b6..e80b7100 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -26,7 +26,7 @@ jobs: uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 22 cache: "pnpm" - name: Move Cypress to dependencies run: | From 70534d8b5a64ed9df64229b8933c6adc2ac61ea4 Mon Sep 17 00:00:00 2001 From: Kesuaheli Date: Thu, 4 Sep 2025 12:51:56 +0200 Subject: [PATCH 158/181] fix: adding support for newer skin profile data structure in player heads --- renderer/viewer/three/worldrendererThree.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index fb6c8e11..39b8e1de 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -767,12 +767,17 @@ export class WorldRendererThree extends WorldRendererCommon { } renderHead (position: Vec3, rotation: number, isWall: boolean, blockEntity) { - const textures = blockEntity.SkullOwner?.Properties?.textures[0] - if (!textures) return + let textureData: string + if (blockEntity.SkullOwner) { + textureData = blockEntity.SkullOwner.Properties?.textures[0]?.Value + } else { + textureData = blockEntity.profile?.properties?.find(p => p.name === 'textures')?.value + } + if (!textureData) return try { - const textureData = JSON.parse(Buffer.from(textures.Value, 'base64').toString()) - let skinUrl = textureData.textures?.SKIN?.url + const decodedData = JSON.parse(Buffer.from(textureData, 'base64').toString()) + let skinUrl = decodedData.textures?.SKIN?.url const { skinTexturesProxy } = this.worldRendererConfig if (skinTexturesProxy) { skinUrl = skinUrl?.replace('http://textures.minecraft.net/', skinTexturesProxy) From 528d8f516b1d8f86552fbf64d8130b138e1aba23 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Thu, 4 Sep 2025 21:55:02 +0300 Subject: [PATCH 159/181] Update worldrendererThree.ts --- renderer/viewer/three/worldrendererThree.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index 39b8e1de..440061ad 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -769,7 +769,7 @@ export class WorldRendererThree extends WorldRendererCommon { renderHead (position: Vec3, rotation: number, isWall: boolean, blockEntity) { let textureData: string if (blockEntity.SkullOwner) { - textureData = blockEntity.SkullOwner.Properties?.textures[0]?.Value + textureData = blockEntity.SkullOwner.Properties?.textures?.[0]?.Value } else { textureData = blockEntity.profile?.properties?.find(p => p.name === 'textures')?.value } From b2e36840b9d3531ed352e08e09fc54734026cc51 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Fri, 5 Sep 2025 05:02:54 +0300 Subject: [PATCH 160/181] feat: brand new default skybox with fog, better daycycle and colors (#425) Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- README.MD | 1 + renderer/viewer/lib/worldDataEmitter.ts | 32 +++ renderer/viewer/lib/worldrendererCommon.ts | 66 +++-- renderer/viewer/three/skyboxRenderer.ts | 281 +++++++++++++++++++- renderer/viewer/three/worldrendererThree.ts | 23 +- src/dayCycle.ts | 46 ---- src/index.ts | 2 - src/watchOptions.ts | 1 + 8 files changed, 375 insertions(+), 77 deletions(-) delete mode 100644 src/dayCycle.ts diff --git a/README.MD b/README.MD index 82aac011..74d4eb41 100644 --- a/README.MD +++ b/README.MD @@ -233,3 +233,4 @@ Only during development: - [https://github.com/ClassiCube/ClassiCube](ClassiCube - Better C# Rewrite) [DEMO](https://www.classicube.net/server/play/?warned=true) - [https://m.eaglercraft.com/](EaglerCraft) - Eaglercraft runnable on mobile (real Minecraft in the browser) +- [js-minecraft](https://github.com/LabyStudio/js-minecraft) - An insanely well done clone from the graphical side that inspired many features here diff --git a/renderer/viewer/lib/worldDataEmitter.ts b/renderer/viewer/lib/worldDataEmitter.ts index 86a85f77..dfbdb35c 100644 --- a/renderer/viewer/lib/worldDataEmitter.ts +++ b/renderer/viewer/lib/worldDataEmitter.ts @@ -7,6 +7,7 @@ import { Vec3 } from 'vec3' import { BotEvents } from 'mineflayer' import { proxy } from 'valtio' import TypedEmitter from 'typed-emitter' +import { Biome } from 'minecraft-data' import { delayedIterator } from '../../playground/shared' import { chunkPos } from './simpleUtils' @@ -28,6 +29,8 @@ export type WorldDataEmitterEvents = { updateLight: (data: { pos: Vec3 }) => void onWorldSwitch: () => void end: () => void + biomeUpdate: (data: { biome: Biome }) => void + biomeReset: () => void } export class WorldDataEmitterWorker extends (EventEmitter as new () => TypedEmitter) { @@ -360,8 +363,37 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter { export const worldCleanup = buildCleanupDecorator('resetWorld') export const defaultWorldRendererConfig = { + // Debug settings showChunkBorders: false, + enableDebugOverlay: false, + + // Performance settings mesherWorkers: 4, - isPlayground: false, - renderEars: true, - skinTexturesProxy: undefined as string | undefined, - // game renderer setting actually - showHand: false, - viewBobbing: false, - extraBlockRenderers: true, - clipWorldBelowY: undefined as number | undefined, + addChunksBatchWaitTime: 200, + _experimentalSmoothChunkLoading: true, + _renderByChunks: false, + + // Rendering engine settings + dayCycle: true, smoothLighting: true, enableLighting: true, starfield: true, - addChunksBatchWaitTime: 200, + renderEntities: true, + extraBlockRenderers: true, + foreground: true, + fov: 75, + volume: 1, + + // Camera visual related settings + showHand: false, + viewBobbing: false, + renderEars: true, + highlightBlockColor: 'blue', + + // Player models + fetchPlayerSkins: true, + skinTexturesProxy: undefined as string | undefined, + + // VR settings vrSupport: true, vrPageGameRendering: true, - renderEntities: true, - fov: 75, - fetchPlayerSkins: true, - highlightBlockColor: 'blue', - foreground: true, - enableDebugOverlay: false, - _experimentalSmoothChunkLoading: true, - _renderByChunks: false, - volume: 1 + + // World settings + clipWorldBelowY: undefined as number | undefined, + isPlayground: false } export type WorldRendererConfig = typeof defaultWorldRendererConfig @@ -496,6 +509,10 @@ export abstract class WorldRendererCommon timeUpdated? (newTime: number): void + biomeUpdated? (biome: any): void + + biomeReset? (): void + updateViewerPosition (pos: Vec3) { this.viewerChunkPosition = pos for (const [key, value] of Object.entries(this.loadedChunks)) { @@ -817,12 +834,9 @@ export abstract class WorldRendererCommon }) worldEmitter.on('time', (timeOfDay) => { + if (!this.worldRendererConfig.dayCycle) return this.timeUpdated?.(timeOfDay) - if (timeOfDay < 0 || timeOfDay > 24_000) { - throw new Error('Invalid time of day. It should be between 0 and 24000.') - } - this.timeOfTheDay = timeOfDay // if (this.worldRendererConfig.skyLight === skyLight) return @@ -831,6 +845,14 @@ export abstract class WorldRendererCommon // (this).rerenderAllChunks?.() // } }) + + worldEmitter.on('biomeUpdate', ({ biome }) => { + this.biomeUpdated?.(biome) + }) + + worldEmitter.on('biomeReset', () => { + this.biomeReset?.() + }) } setBlockStateIdInner (pos: Vec3, stateId: number | undefined, needAoRecalculation = true) { diff --git a/renderer/viewer/three/skyboxRenderer.ts b/renderer/viewer/three/skyboxRenderer.ts index 294c72aa..aa8c3bb6 100644 --- a/renderer/viewer/three/skyboxRenderer.ts +++ b/renderer/viewer/three/skyboxRenderer.ts @@ -1,10 +1,28 @@ import * as THREE from 'three' +export const DEFAULT_TEMPERATURE = 0.75 + export class SkyboxRenderer { private texture: THREE.Texture | null = null private mesh: THREE.Mesh | null = null + private skyMesh: THREE.Mesh | null = null + private voidMesh: THREE.Mesh | null = null - constructor (private readonly scene: THREE.Scene, public initialImage: string | null) {} + // World state + private worldTime = 0 + private partialTicks = 0 + private viewDistance = 4 + private temperature = DEFAULT_TEMPERATURE + private inWater = false + private waterBreathing = false + private fogBrightness = 0 + private prevFogBrightness = 0 + + constructor (private readonly scene: THREE.Scene, public initialImage: string | null) { + if (!initialImage) { + this.createGradientSky() + } + } async init () { if (this.initialImage) { @@ -58,10 +76,255 @@ export class SkyboxRenderer { } } - update (cameraPosition: THREE.Vector3) { - if (this.mesh) { - this.mesh.position.copy(cameraPosition) + update (cameraPosition: THREE.Vector3, newViewDistance: number) { + if (newViewDistance !== this.viewDistance) { + this.viewDistance = newViewDistance + this.updateSkyColors() } + + if (this.mesh) { + // Update skybox position + this.mesh.position.copy(cameraPosition) + } else if (this.skyMesh) { + // Update gradient sky position + this.skyMesh.position.copy(cameraPosition) + this.voidMesh?.position.copy(cameraPosition) + this.updateSkyColors() // Update colors based on time of day + } + } + + // Update world time + updateTime (timeOfDay: number, partialTicks = 0) { + this.worldTime = timeOfDay + this.partialTicks = partialTicks + this.updateSkyColors() + } + + // Update view distance + updateViewDistance (viewDistance: number) { + this.viewDistance = viewDistance + this.updateSkyColors() + } + + // Update temperature (for biome support) + updateTemperature (temperature: number) { + this.temperature = temperature + this.updateSkyColors() + } + + // Update water state + updateWaterState (inWater: boolean, waterBreathing: boolean) { + this.inWater = inWater + this.waterBreathing = waterBreathing + this.updateSkyColors() + } + + private createGradientSky () { + const size = 64 + const scale = 256 / size + 2 + + { + const geometry = new THREE.PlaneGeometry(size * scale * 2, size * scale * 2) + geometry.rotateX(-Math.PI / 2) + geometry.translate(0, 16, 0) + + const material = new THREE.MeshBasicMaterial({ + color: 0xff_ff_ff, + side: THREE.DoubleSide, + depthTest: false + }) + + this.skyMesh = new THREE.Mesh(geometry, material) + this.scene.add(this.skyMesh) + } + + { + const geometry = new THREE.PlaneGeometry(size * scale * 2, size * scale * 2) + geometry.rotateX(-Math.PI / 2) + geometry.translate(0, -16, 0) + + const material = new THREE.MeshBasicMaterial({ + color: 0xff_ff_ff, + side: THREE.DoubleSide, + depthTest: false + }) + + this.voidMesh = new THREE.Mesh(geometry, material) + this.scene.add(this.voidMesh) + } + + this.updateSkyColors() + } + + private getFogColor (partialTicks = 0): THREE.Vector3 { + const angle = this.getCelestialAngle(partialTicks) + let rotation = Math.cos(angle * Math.PI * 2) * 2 + 0.5 + rotation = Math.max(0, Math.min(1, rotation)) + + let x = 0.752_941_2 + let y = 0.847_058_83 + let z = 1 + + x *= (rotation * 0.94 + 0.06) + y *= (rotation * 0.94 + 0.06) + z *= (rotation * 0.91 + 0.09) + + return new THREE.Vector3(x, y, z) + } + + private getSkyColor (x = 0, z = 0, partialTicks = 0): THREE.Vector3 { + const angle = this.getCelestialAngle(partialTicks) + let brightness = Math.cos(angle * 3.141_593 * 2) * 2 + 0.5 + + if (brightness < 0) brightness = 0 + if (brightness > 1) brightness = 1 + + const temperature = this.getTemperature(x, z) + const rgb = this.getSkyColorByTemp(temperature) + + const red = ((rgb >> 16) & 0xff) / 255 + const green = ((rgb >> 8) & 0xff) / 255 + const blue = (rgb & 0xff) / 255 + + return new THREE.Vector3( + red * brightness, + green * brightness, + blue * brightness + ) + } + + private calculateCelestialAngle (time: number, partialTicks: number): number { + const modTime = (time % 24_000) + let angle = (modTime + partialTicks) / 24_000 - 0.25 + + if (angle < 0) { + angle++ + } + if (angle > 1) { + angle-- + } + + angle = 1 - ((Math.cos(angle * Math.PI) + 1) / 2) + angle += (angle - angle) / 3 + + return angle + } + + private getCelestialAngle (partialTicks: number): number { + return this.calculateCelestialAngle(this.worldTime, partialTicks) + } + + private getTemperature (x: number, z: number): number { + return this.temperature + } + + private getSkyColorByTemp (temperature: number): number { + temperature /= 3 + if (temperature < -1) temperature = -1 + if (temperature > 1) temperature = 1 + + const hue = 0.622_222_2 - temperature * 0.05 + const saturation = 0.5 + temperature * 0.1 + const brightness = 1 + + return this.hsbToRgb(hue, saturation, brightness) + } + + private hsbToRgb (hue: number, saturation: number, brightness: number): number { + let r = 0; let g = 0; let b = 0 + if (saturation === 0) { + r = g = b = Math.floor(brightness * 255 + 0.5) + } else { + const h = (hue - Math.floor(hue)) * 6 + const f = h - Math.floor(h) + const p = brightness * (1 - saturation) + const q = brightness * (1 - saturation * f) + const t = brightness * (1 - (saturation * (1 - f))) + switch (Math.floor(h)) { + case 0: + r = Math.floor(brightness * 255 + 0.5) + g = Math.floor(t * 255 + 0.5) + b = Math.floor(p * 255 + 0.5) + break + case 1: + r = Math.floor(q * 255 + 0.5) + g = Math.floor(brightness * 255 + 0.5) + b = Math.floor(p * 255 + 0.5) + break + case 2: + r = Math.floor(p * 255 + 0.5) + g = Math.floor(brightness * 255 + 0.5) + b = Math.floor(t * 255 + 0.5) + break + case 3: + r = Math.floor(p * 255 + 0.5) + g = Math.floor(q * 255 + 0.5) + b = Math.floor(brightness * 255 + 0.5) + break + case 4: + r = Math.floor(t * 255 + 0.5) + g = Math.floor(p * 255 + 0.5) + b = Math.floor(brightness * 255 + 0.5) + break + case 5: + r = Math.floor(brightness * 255 + 0.5) + g = Math.floor(p * 255 + 0.5) + b = Math.floor(q * 255 + 0.5) + break + } + } + return 0xff_00_00_00 | (r << 16) | (g << 8) | (Math.trunc(b)) + } + + private updateSkyColors () { + if (!this.skyMesh || !this.voidMesh) return + + // Update fog brightness with smooth transition + this.prevFogBrightness = this.fogBrightness + const renderDistance = this.viewDistance / 32 + const brightnessAtPosition = 1 // Could be affected by light level in future + const targetBrightness = brightnessAtPosition * (1 - renderDistance) + renderDistance + this.fogBrightness += (targetBrightness - this.fogBrightness) * 0.1 + + // Handle water fog + if (this.inWater) { + const waterViewDistance = this.waterBreathing ? 100 : 5 + this.scene.fog = new THREE.Fog(new THREE.Color(0, 0, 1), 0.0025, waterViewDistance) + this.scene.background = new THREE.Color(0, 0, 1) + + // Update sky and void colors for underwater effect + ;(this.skyMesh.material as THREE.MeshBasicMaterial).color.set(new THREE.Color(0, 0, 1)) + ;(this.voidMesh.material as THREE.MeshBasicMaterial).color.set(new THREE.Color(0, 0, 0.6)) + return + } + + // Normal sky colors + const viewDistance = this.viewDistance * 16 + const viewFactor = 1 - (0.25 + 0.75 * this.viewDistance / 32) ** 0.25 + + const angle = this.getCelestialAngle(this.partialTicks) + const skyColor = this.getSkyColor(0, 0, this.partialTicks) + const fogColor = this.getFogColor(this.partialTicks) + + const brightness = Math.cos(angle * Math.PI * 2) * 2 + 0.5 + const clampedBrightness = Math.max(0, Math.min(1, brightness)) + + // Interpolate fog brightness + const interpolatedBrightness = this.prevFogBrightness + (this.fogBrightness - this.prevFogBrightness) * this.partialTicks + + const red = (fogColor.x + (skyColor.x - fogColor.x) * viewFactor) * clampedBrightness * interpolatedBrightness + const green = (fogColor.y + (skyColor.y - fogColor.y) * viewFactor) * clampedBrightness * interpolatedBrightness + const blue = (fogColor.z + (skyColor.z - fogColor.z) * viewFactor) * clampedBrightness * interpolatedBrightness + + this.scene.background = new THREE.Color(red, green, blue) + this.scene.fog = new THREE.Fog(new THREE.Color(red, green, blue), 0.0025, viewDistance * 2) + + ;(this.skyMesh.material as THREE.MeshBasicMaterial).color.set(new THREE.Color(skyColor.x, skyColor.y, skyColor.z)) + ;(this.voidMesh.material as THREE.MeshBasicMaterial).color.set(new THREE.Color( + skyColor.x * 0.2 + 0.04, + skyColor.y * 0.2 + 0.04, + skyColor.z * 0.6 + 0.1 + )) } dispose () { @@ -73,5 +336,15 @@ export class SkyboxRenderer { ;(this.mesh.material as THREE.Material).dispose() this.scene.remove(this.mesh) } + if (this.skyMesh) { + this.skyMesh.geometry.dispose() + ;(this.skyMesh.material as THREE.Material).dispose() + this.scene.remove(this.skyMesh) + } + if (this.voidMesh) { + this.voidMesh.geometry.dispose() + ;(this.voidMesh.material as THREE.Material).dispose() + this.scene.remove(this.voidMesh) + } } } diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index 440061ad..29e9223c 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -3,6 +3,7 @@ import { Vec3 } from 'vec3' import nbt from 'prismarine-nbt' import PrismarineChatLoader from 'prismarine-chat' import * as tweenJs from '@tweenjs/tween.js' +import { Biome } from 'minecraft-data' import { renderSign } from '../sign-renderer' import { DisplayWorldOptions, GraphicsInitOptions } from '../../../src/appViewer' import { chunkPos, sectionPos } from '../lib/simpleUtils' @@ -24,7 +25,7 @@ import { CameraShake } from './cameraShake' import { ThreeJsMedia } from './threeJsMedia' import { Fountain } from './threeJsParticles' import { WaypointsRenderer } from './waypoints' -import { SkyboxRenderer } from './skyboxRenderer' +import { DEFAULT_TEMPERATURE, SkyboxRenderer } from './skyboxRenderer' type SectionKey = string @@ -173,7 +174,10 @@ export class WorldRendererThree extends WorldRendererCommon { override watchReactivePlayerState () { super.watchReactivePlayerState() this.onReactivePlayerStateUpdated('inWater', (value) => { - this.scene.fog = value ? new THREE.Fog(0x00_00_ff, 0.1, this.playerStateReactive.waterBreathing ? 100 : 20) : null + this.skyboxRenderer.updateWaterState(value, this.playerStateReactive.waterBreathing) + }) + this.onReactivePlayerStateUpdated('waterBreathing', (value) => { + this.skyboxRenderer.updateWaterState(this.playerStateReactive.inWater, value) }) this.onReactivePlayerStateUpdated('ambientLight', (value) => { if (!value) return @@ -264,6 +268,19 @@ export class WorldRendererThree extends WorldRendererCommon { } else { this.starField.remove() } + + this.skyboxRenderer.updateTime(newTime) + } + + biomeUpdated (biome: Biome): void { + if (biome?.temperature !== undefined) { + this.skyboxRenderer.updateTemperature(biome.temperature) + } + } + + biomeReset (): void { + // Reset to default temperature when biome is unknown + this.skyboxRenderer.updateTemperature(DEFAULT_TEMPERATURE) } getItemRenderData (item: Record, specificProps: ItemSpecificContextProperties) { @@ -716,7 +733,7 @@ export class WorldRendererThree extends WorldRendererCommon { // Update skybox position to follow camera const cameraPos = this.getCameraPosition() - this.skyboxRenderer.update(cameraPos) + this.skyboxRenderer.update(cameraPos, this.viewDistance) const sizeOrFovChanged = sizeChanged || this.displayOptions.inWorldRenderingConfig.fov !== this.camera.fov if (sizeOrFovChanged) { diff --git a/src/dayCycle.ts b/src/dayCycle.ts deleted file mode 100644 index 50e63a21..00000000 --- a/src/dayCycle.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { options } from './optionsStorage' -import { assertDefined } from './utils' -import { updateBackground } from './water' - -export default () => { - const timeUpdated = () => { - // 0 morning - const dayTotal = 24_000 - const evening = 11_500 - const night = 13_500 - const morningStart = 23_000 - const morningEnd = 23_961 - const timeProgress = options.dayCycleAndLighting ? bot.time.timeOfDay : 0 - - // todo check actual colors - const dayColorRainy = { r: 111 / 255, g: 156 / 255, b: 236 / 255 } - // todo yes, we should make animations (and rain) - // eslint-disable-next-line unicorn/numeric-separators-style - const dayColor = bot.isRaining ? dayColorRainy : { r: 0.6784313725490196, g: 0.8470588235294118, b: 0.9019607843137255 } // lightblue - // let newColor = dayColor - let int = 1 - if (timeProgress < evening) { - // stay dayily - } else if (timeProgress < night) { - const progressNorm = timeProgress - evening - const progressMax = night - evening - int = 1 - progressNorm / progressMax - } else if (timeProgress < morningStart) { - int = 0 - } else if (timeProgress < morningEnd) { - const progressNorm = timeProgress - morningStart - const progressMax = night - morningEnd - int = progressNorm / progressMax - } - // todo need to think wisely how to set these values & also move directional light around! - const colorInt = Math.max(int, 0.1) - updateBackground({ r: dayColor.r * colorInt, g: dayColor.g * colorInt, b: dayColor.b * colorInt }) - if (!options.newVersionsLighting && bot.supportFeature('blockStateId')) { - appViewer.playerState.reactive.ambientLight = Math.max(int, 0.25) - appViewer.playerState.reactive.directionalLight = Math.min(int, 0.5) - } - } - - bot.on('time', timeUpdated) - timeUpdated() -} diff --git a/src/index.ts b/src/index.ts index 54731a16..7764188f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -56,7 +56,6 @@ import { isCypress } from './standaloneUtils' import { startLocalServer, unsupportedLocalServerFeatures } from './createLocalServer' import defaultServerOptions from './defaultLocalServerOptions' -import dayCycle from './dayCycle' import { onAppLoad, resourcepackReload, resourcePackState } from './resourcePack' import { ConnectPeerOptions, connectToPeer } from './localServerMultiplayer' @@ -794,7 +793,6 @@ export async function connect (connectOptions: ConnectOptions) { } initMotionTracking() - dayCycle() // Bot position callback const botPosition = () => { diff --git a/src/watchOptions.ts b/src/watchOptions.ts index 2f5199c0..da75cc74 100644 --- a/src/watchOptions.ts +++ b/src/watchOptions.ts @@ -128,5 +128,6 @@ export const watchOptionsAfterWorldViewInit = (worldView: WorldDataEmitter) => { appViewer.inWorldRenderingConfig.renderEars = o.renderEars appViewer.inWorldRenderingConfig.showHand = o.showHand appViewer.inWorldRenderingConfig.viewBobbing = o.viewBobbing + appViewer.inWorldRenderingConfig.dayCycle = o.dayCycleAndLighting }) } From 265d02d18d46fbde79437857eb58254fb68b1cf0 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Sun, 7 Sep 2025 18:23:13 +0000 Subject: [PATCH 161/181] up protocol for 1.21.8 --- pnpm-lock.yaml | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6188b6d..fd8be62b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -140,13 +140,13 @@ importers: version: 4.17.21 mcraft-fun-mineflayer: specifier: ^0.1.23 - version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13)) + version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/4493c8ccbd6f7e775d76149d838f4386ae959f0e(encoding@0.1.13)) minecraft-data: specifier: 3.92.0 version: 3.92.0 minecraft-protocol: specifier: github:PrismarineJS/node-minecraft-protocol#master - version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13) + version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13) mineflayer-item-map-downloader: specifier: github:zardoy/mineflayer-item-map-downloader version: https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad)(encoding@0.1.13) @@ -345,7 +345,7 @@ importers: version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/89c33d396f3fde4804c71f4be3c203ade1833b41(@types/react@18.3.18)(react@18.3.1) mineflayer: specifier: github:zardoy/mineflayer#gen-the-master - version: https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13) + version: https://codeload.github.com/zardoy/mineflayer/tar.gz/4493c8ccbd6f7e775d76149d838f4386ae959f0e(encoding@0.1.13) mineflayer-mouse: specifier: ^0.1.21 version: 0.1.21 @@ -6668,8 +6668,8 @@ packages: resolution: {tarball: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/89c33d396f3fde4804c71f4be3c203ade1833b41} version: 1.0.1 - minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074: - resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074} + minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9: + resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9} version: 1.61.0 engines: {node: '>=22'} @@ -6688,8 +6688,8 @@ packages: resolution: {integrity: sha512-1XTVuw3twIrEcqQ1QRSB8NcStIUEZ+tbxiAG6rOrN/9M4thhtlS5PTJzFdmdrcYgWEBLvuOdJszaKE5zFfiXhg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8: - resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8} + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/4493c8ccbd6f7e775d76149d838f4386ae959f0e: + resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/4493c8ccbd6f7e775d76149d838f4386ae959f0e} version: 8.0.0 engines: {node: '>=22'} @@ -11344,7 +11344,7 @@ snapshots: dependencies: '@nxg-org/mineflayer-util-plugin': 1.8.4 minecraft-data: 3.92.0 - mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13) + mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/4493c8ccbd6f7e775d76149d838f4386ae959f0e(encoding@0.1.13) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-item: 1.17.0 prismarine-physics: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b @@ -13106,7 +13106,7 @@ snapshots: long: 5.3.1 mc-bridge: 0.1.3(minecraft-data@3.92.0) minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 @@ -13143,7 +13143,7 @@ snapshots: flatmap: 0.0.3 long: 5.3.1 minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 @@ -16990,12 +16990,12 @@ snapshots: dependencies: minecraft-data: 3.92.0 - mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13)): + mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/4493c8ccbd6f7e775d76149d838f4386ae959f0e(encoding@0.1.13)): dependencies: '@zardoy/flying-squid': 0.0.49(encoding@0.1.13) exit-hook: 2.2.1 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13) - mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13) + mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/4493c8ccbd6f7e775d76149d838f4386ae959f0e(encoding@0.1.13) prismarine-item: 1.17.0 ws: 8.18.1 transitivePeerDependencies: @@ -17313,7 +17313,7 @@ snapshots: - '@types/react' - react - minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13): + minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13): dependencies: '@types/node-rsa': 1.1.4 '@types/readable-stream': 4.0.18 @@ -17365,7 +17365,7 @@ snapshots: mineflayer-item-map-downloader@https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad)(encoding@0.1.13): dependencies: - mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13) + mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/4493c8ccbd6f7e775d76149d838f4386ae959f0e(encoding@0.1.13) sharp: 0.30.7 transitivePeerDependencies: - encoding @@ -17380,11 +17380,11 @@ snapshots: transitivePeerDependencies: - supports-color - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13): + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/4493c8ccbd6f7e775d76149d838f4386ae959f0e(encoding@0.1.13): dependencies: '@nxg-org/mineflayer-physics-util': 1.8.10 minecraft-data: 3.92.0 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13) prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chat: 1.11.0 From 7f7a14ac65105649621ed825245ca23c2f2c5f85 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Mon, 8 Sep 2025 04:19:38 +0300 Subject: [PATCH 162/181] feat: Add overlay model viewer. Already integrated into inventory to display player! --- renderer/viewer/lib/createPlayerObject.ts | 55 +++ renderer/viewer/three/entities.ts | 7 +- src/inventoryWindows.ts | 35 ++ src/react/OverlayModelViewer.tsx | 505 ++++++++++++++++++++++ src/reactUi.tsx | 2 + 5 files changed, 598 insertions(+), 6 deletions(-) create mode 100644 renderer/viewer/lib/createPlayerObject.ts create mode 100644 src/react/OverlayModelViewer.tsx diff --git a/renderer/viewer/lib/createPlayerObject.ts b/renderer/viewer/lib/createPlayerObject.ts new file mode 100644 index 00000000..836c8062 --- /dev/null +++ b/renderer/viewer/lib/createPlayerObject.ts @@ -0,0 +1,55 @@ +import { PlayerObject, PlayerAnimation } from 'skinview3d' +import * as THREE from 'three' +import { WalkingGeneralSwing } from '../three/entity/animations' +import { loadSkinImage, stevePngUrl } from './utils/skins' + +export type PlayerObjectType = PlayerObject & { + animation?: PlayerAnimation + realPlayerUuid: string + realUsername: string +} + +export function createPlayerObject (options: { + username?: string + uuid?: string + scale?: number +}): { + playerObject: PlayerObjectType + wrapper: THREE.Group + } { + const wrapper = new THREE.Group() + const playerObject = new PlayerObject() as PlayerObjectType + + playerObject.realPlayerUuid = options.uuid ?? '' + playerObject.realUsername = options.username ?? '' + playerObject.position.set(0, 16, 0) + + // fix issues with starfield + playerObject.traverse((obj) => { + if (obj instanceof THREE.Mesh && obj.material instanceof THREE.MeshStandardMaterial) { + obj.material.transparent = true + } + }) + + wrapper.add(playerObject as any) + const scale = options.scale ?? (1 / 16) + wrapper.scale.set(scale, scale, scale) + wrapper.rotation.set(0, Math.PI, 0) + + // Set up animation + playerObject.animation = new WalkingGeneralSwing() + ;(playerObject.animation as WalkingGeneralSwing).isMoving = false + playerObject.animation.update(playerObject, 0) + + return { playerObject, wrapper } +} + +export const applySkinToPlayerObject = async (playerObject: PlayerObjectType, skinUrl: string) => { + return loadSkinImage(skinUrl || stevePngUrl).then(({ canvas }) => { + const skinTexture = new THREE.CanvasTexture(canvas) + skinTexture.magFilter = THREE.NearestFilter + skinTexture.minFilter = THREE.NearestFilter + skinTexture.needsUpdate = true + playerObject.skin.map = skinTexture as any + }).catch(console.error) +} diff --git a/renderer/viewer/three/entities.ts b/renderer/viewer/three/entities.ts index 7849686b..fad30182 100644 --- a/renderer/viewer/three/entities.ts +++ b/renderer/viewer/three/entities.ts @@ -20,6 +20,7 @@ import { ItemSpecificContextProperties } from '../lib/basePlayerState' import { loadSkinFromUsername, loadSkinImage, stevePngUrl } from '../lib/utils/skins' import { renderComponent } from '../sign-renderer' import { createCanvas } from '../lib/utils' +import { PlayerObjectType } from '../lib/createPlayerObject' import { getBlockMeshFromModel } from './holdingBlock' import { createItemMesh } from './itemMesh' import * as Entity from './entity/EntityMesh' @@ -33,12 +34,6 @@ export const steveTexture = loadThreeJsTextureFromUrl(stevePngUrl) export const TWEEN_DURATION = 120 -type PlayerObjectType = PlayerObject & { - animation?: PlayerAnimation - realPlayerUuid: string - realUsername: string -} - function convert2sComplementToHex (complement: number) { if (complement < 0) { complement = (0xFF_FF_FF_FF + complement + 1) >>> 0 diff --git a/src/inventoryWindows.ts b/src/inventoryWindows.ts index 166e42a7..d16fee20 100644 --- a/src/inventoryWindows.ts +++ b/src/inventoryWindows.ts @@ -12,6 +12,7 @@ import PrismarineChatLoader from 'prismarine-chat' import * as nbt from 'prismarine-nbt' import { BlockModel } from 'mc-assets' import { renderSlot } from 'renderer/viewer/three/renderSlot' +import { loadSkinFromUsername } from 'renderer/viewer/lib/utils/skins' import Generic95 from '../assets/generic_95.png' import { appReplacableResources } from './generated/resources' import { activeModalStack, hideCurrentModal, hideModal, miscUiState, showModal } from './globalState' @@ -23,6 +24,7 @@ import { getItemDescription } from './itemsDescriptions' import { MessageFormatPart } from './chatUtils' import { GeneralInputItem, getItemMetadata, getItemModelName, getItemNameRaw, RenderItem } from './mineflayer/items' import { playerState } from './mineflayer/playerState' +import { modelViewerState } from './react/OverlayModelViewer' const loadedImagesCache = new Map() const cleanLoadedImagesCache = () => { @@ -40,6 +42,34 @@ export const jeiCustomCategories = proxy({ value: [] as Array<{ id: string, categoryTitle: string, items: any[] }> }) +let remotePlayerSkin: string | undefined | Promise + +export const showInventoryPlayer = () => { + modelViewerState.model = { + positioning: { + windowWidth: 176, + windowHeight: 166, + x: 25, + y: 8, + width: 50, + height: 70, + scaled: true, + onlyInitialScale: true, + followCursor: true, + }, + // models: ['https://bucket.mcraft.fun/sitarbuckss.glb'], + // debug: true, + steveModelSkin: appViewer.playerState.reactive.playerSkin ?? (typeof remotePlayerSkin === 'string' ? remotePlayerSkin : ''), + } + if (remotePlayerSkin === undefined && !appViewer.playerState.reactive.playerSkin) { + remotePlayerSkin = loadSkinFromUsername(bot.username, 'skin').then(a => { + setTimeout(() => { showInventoryPlayer() }, 0) // todo patch instead and make reactive + remotePlayerSkin = a ?? '' + return remotePlayerSkin + }) + } +} + export const onGameLoad = () => { version = bot.version @@ -392,7 +422,12 @@ const openWindow = (type: string | undefined, title: string | any = undefined) = miscUiState.displaySearchInput = false destroyFn() skipClosePacketSending = false + + modelViewerState.model = undefined }) + if (type === undefined) { + showInventoryPlayer() + } cleanLoadedImagesCache() const inv = openItemsCanvas(type) inv.canvasManager.children[0].mobileHelpers = miscUiState.currentTouch diff --git a/src/react/OverlayModelViewer.tsx b/src/react/OverlayModelViewer.tsx new file mode 100644 index 00000000..24dc836d --- /dev/null +++ b/src/react/OverlayModelViewer.tsx @@ -0,0 +1,505 @@ +import { proxy, useSnapshot, subscribe } from 'valtio' +import { useEffect, useMemo, useRef } from 'react' +import * as THREE from 'three' +import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' +import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader' +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader' +import { applySkinToPlayerObject, createPlayerObject, PlayerObjectType } from '../../renderer/viewer/lib/createPlayerObject' +import { currentScaling } from '../scaleInterface' + +THREE.ColorManagement.enabled = false + +export const modelViewerState = proxy({ + model: undefined as undefined | { + models?: string[] // Array of model URLs (URL itself is the cache key) + steveModelSkin?: string + debug?: boolean + // absolute positioning + positioning: { + windowWidth: number + windowHeight: number + x: number + y: number + width: number + height: number + scaled?: boolean + onlyInitialScale?: boolean + followCursor?: boolean + } + modelCustomization?: { [modelUrl: string]: { color?: string, opacity?: number, metalness?: number, roughness?: number } } + resetRotationOnReleae?: boolean + continiousRender?: boolean + } +}) +globalThis.modelViewerState = modelViewerState + +// Global debug function to get camera and model values +globalThis.getModelViewerValues = () => { + const scene = globalThis.sceneRef?.current + if (!scene) return null + + const { camera, playerObject } = scene + if (!playerObject) return null + + const wrapper = playerObject.parent + if (!wrapper) return null + + const box = new THREE.Box3().setFromObject(wrapper) + const size = box.getSize(new THREE.Vector3()) + const center = box.getCenter(new THREE.Vector3()) + + return { + camera: { + position: camera.position.clone(), + fov: camera.fov, + aspect: camera.aspect + }, + model: { + position: wrapper.position.clone(), + rotation: wrapper.rotation.clone(), + scale: wrapper.scale.clone(), + size, + center + }, + cursor: { + position: globalThis.cursorPosition || { x: 0, y: 0 }, + normalized: globalThis.cursorPosition ? { + x: globalThis.cursorPosition.x * 2 - 1, + y: globalThis.cursorPosition.y * 2 - 1 + } : { x: 0, y: 0 } + }, + visibleArea: { + height: 2 * Math.tan(camera.fov * Math.PI / 180 / 2) * camera.position.z, + width: 2 * Math.tan(camera.fov * Math.PI / 180 / 2) * camera.position.z * camera.aspect + } + } +} + +export default () => { + const { model } = useSnapshot(modelViewerState) + const containerRef = useRef(null) + const sceneRef = useRef<{ + scene: THREE.Scene + camera: THREE.PerspectiveCamera + renderer: THREE.WebGLRenderer + controls: OrbitControls + playerObject?: PlayerObjectType + dispose: () => void + }>() + const initialScale = useMemo(() => { + return currentScaling.scale + }, []) + globalThis.sceneRef = sceneRef + + // Cursor following state + const cursorPosition = useRef({ x: 0, y: 0 }) + const isFollowingCursor = useRef(false) + + // Model management state + const loadedModels = useRef>(new Map()) + const modelLoaders = useRef>(new Map()) + + // Model management functions + const loadModel = (modelUrl: string) => { + if (loadedModels.current.has(modelUrl)) return // Already loaded + + const isGLTF = modelUrl.toLowerCase().endsWith('.gltf') || modelUrl.toLowerCase().endsWith('.glb') + const loader = isGLTF ? new GLTFLoader() : new OBJLoader() + modelLoaders.current.set(modelUrl, loader) + + const onLoad = (object: THREE.Object3D) => { + // Apply customization if available + const customization = model?.modelCustomization?.[modelUrl] + if (customization) { + object.traverse((child) => { + if (child instanceof THREE.Mesh && child.material) { + const material = child.material as THREE.MeshStandardMaterial + if (customization.color) { + material.color.setHex(parseInt(customization.color.replace('#', ''), 16)) + } + if (customization.opacity !== undefined) { + material.opacity = customization.opacity + material.transparent = customization.opacity < 1 + } + if (customization.metalness !== undefined) { + material.metalness = customization.metalness + } + if (customization.roughness !== undefined) { + material.roughness = customization.roughness + } + } + }) + } + + // Center and scale model + const box = new THREE.Box3().setFromObject(object) + const center = box.getCenter(new THREE.Vector3()) + const size = box.getSize(new THREE.Vector3()) + const maxDim = Math.max(size.x, size.y, size.z) + const scale = 2 / maxDim + object.scale.setScalar(scale) + object.position.sub(center.multiplyScalar(scale)) + + // Store the model using URL as key + loadedModels.current.set(modelUrl, object) + sceneRef.current?.scene.add(object) + + // Trigger render + if (sceneRef.current) { + setTimeout(() => { + const render = () => sceneRef.current?.renderer.render(sceneRef.current.scene, sceneRef.current.camera) + render() + }, 0) + } + } + + if (isGLTF) { + (loader as GLTFLoader).load(modelUrl, (gltf) => { + onLoad(gltf.scene) + }) + } else { + (loader as OBJLoader).load(modelUrl, onLoad) + } + } + + const removeModel = (modelUrl: string) => { + const model = loadedModels.current.get(modelUrl) + if (model) { + sceneRef.current?.scene.remove(model) + model.traverse((child) => { + if (child instanceof THREE.Mesh) { + if (child.material) { + if (Array.isArray(child.material)) { + for (const mat of child.material) { + mat.dispose() + } + } else { + child.material.dispose() + } + } + if (child.geometry) { + child.geometry.dispose() + } + } + }) + loadedModels.current.delete(modelUrl) + } + modelLoaders.current.delete(modelUrl) + } + + // Subscribe to model changes + useEffect(() => { + if (!modelViewerState.model?.models) return + + const modelsChanged = () => { + const currentModels = modelViewerState.model?.models || [] + const currentModelUrls = new Set(currentModels) + const loadedModelUrls = new Set(loadedModels.current.keys()) + + // Remove models that are no longer in the state + for (const modelUrl of loadedModelUrls) { + if (!currentModelUrls.has(modelUrl)) { + removeModel(modelUrl) + } + } + + // Add new models + for (const modelUrl of currentModels) { + if (!loadedModelUrls.has(modelUrl)) { + loadModel(modelUrl) + } + } + } + const unsubscribe = subscribe(modelViewerState.model.models, modelsChanged) + + let unmounted = false + setTimeout(() => { + if (unmounted) return + modelsChanged() + }) + + return () => { + unmounted = true + unsubscribe?.() + } + }, [model?.models]) + + useEffect(() => { + if (!model || !containerRef.current) return + + // Setup scene + const scene = new THREE.Scene() + scene.background = null // Transparent background + + // Setup camera with optimal settings for player model viewing + const camera = new THREE.PerspectiveCamera( + 50, // Reduced FOV for better model viewing + model.positioning.width / model.positioning.height, + 0.1, + 1000 + ) + camera.position.set(0, 0, 3) // Position camera to view player model optimally + + // Setup renderer with pixel density awareness + const renderer = new THREE.WebGLRenderer({ alpha: true }) + let scale = window.devicePixelRatio || 1 + if (modelViewerState.model?.positioning.scaled) { + scale *= currentScaling.scale + } + renderer.setPixelRatio(scale) + renderer.setSize(model.positioning.width, model.positioning.height) + containerRef.current.appendChild(renderer.domElement) + + // Setup controls + const controls = new OrbitControls(camera, renderer.domElement) + // controls.enableZoom = false + // controls.enablePan = false + controls.minPolarAngle = Math.PI / 2 // Lock vertical rotation + controls.maxPolarAngle = Math.PI / 2 + controls.enableDamping = true + controls.dampingFactor = 0.05 + + // Add ambient light + const ambientLight = new THREE.AmbientLight(0xff_ff_ff, 1) + scene.add(ambientLight) + + // Cursor following function + const updatePlayerLookAt = () => { + if (!isFollowingCursor.current || !sceneRef.current?.playerObject) return + + const { playerObject } = sceneRef.current + const { x, y } = cursorPosition.current + + // Convert 0-1 cursor position to normalized coordinates (-1 to 1) + const normalizedX = x * 2 - 1 + const normalizedY = y * 2 - 1 // Inverted: top of screen = negative pitch, bottom = positive pitch + + // Calculate head rotation based on cursor position + // Limit head movement to realistic angles + const maxHeadYaw = Math.PI / 3 // 60 degrees + const maxHeadPitch = Math.PI / 4 // 45 degrees + + const headYaw = normalizedX * maxHeadYaw + const headPitch = normalizedY * maxHeadPitch + + // Apply head rotation with smooth interpolation + const lerpFactor = 0.1 // Smooth interpolation factor + playerObject.skin.head.rotation.y = THREE.MathUtils.lerp( + playerObject.skin.head.rotation.y, + headYaw, + lerpFactor + ) + playerObject.skin.head.rotation.x = THREE.MathUtils.lerp( + playerObject.skin.head.rotation.x, + headPitch, + lerpFactor + ) + + // Apply slight body rotation for more natural movement + const bodyYaw = headYaw * 0.3 // Body follows head but with less rotation + playerObject.rotation.y = THREE.MathUtils.lerp( + playerObject.rotation.y, + bodyYaw, + lerpFactor * 0.5 // Slower body movement + ) + + render() + } + + // Render function + const render = () => { + renderer.render(scene, camera) + } + + // Setup animation/render strategy + if (model.continiousRender) { + // Continuous animation loop + const animate = () => { + requestAnimationFrame(animate) + render() + } + animate() + } else { + // Render only on camera movement + controls.addEventListener('change', render) + // Initial render + render() + // Render after model loads + if (model.steveModelSkin !== undefined) { + // Create player model + const { playerObject, wrapper } = createPlayerObject({ + scale: 1 // Start with base scale, will adjust below + }) + + // Calculate proper scale and positioning for camera view + const box = new THREE.Box3().setFromObject(wrapper) + const size = box.getSize(new THREE.Vector3()) + const center = box.getCenter(new THREE.Vector3()) + + // Calculate scale to fit within camera view (considering FOV and distance) + const cameraDistance = camera.position.z + const fov = camera.fov * Math.PI / 180 // Convert to radians + const visibleHeight = 2 * Math.tan(fov / 2) * cameraDistance + const visibleWidth = visibleHeight * (model.positioning.width / model.positioning.height) + + const scaleFactor = Math.min( + (visibleHeight) / size.y, + (visibleWidth) / size.x + ) + + wrapper.scale.multiplyScalar(scaleFactor) + + // Center the player object + wrapper.position.sub(center.multiplyScalar(scaleFactor)) + + // Rotate to face camera (remove the default 180° rotation) + wrapper.rotation.set(0, 0, 0) + + scene.add(wrapper) + sceneRef.current = { + ...sceneRef.current!, + playerObject + } + + void applySkinToPlayerObject(playerObject, model.steveModelSkin).then(() => { + setTimeout(render, 0) + }) + + // Set up cursor following if enabled + if (model.positioning.followCursor) { + isFollowingCursor.current = true + } + } + } + + // Window cursor tracking for followCursor + let lastCursorUpdate = 0 + let waitingRender = false + const handleWindowPointerMove = (event: PointerEvent) => { + if (!model.positioning.followCursor) return + + // Track cursor position as 0-1 across the entire window + const newPosition = { + x: event.clientX / window.innerWidth, + y: event.clientY / window.innerHeight + } + cursorPosition.current = newPosition + globalThis.cursorPosition = newPosition // Expose for debug + lastCursorUpdate = Date.now() + updatePlayerLookAt() + if (!waitingRender) { + requestAnimationFrame(() => { + render() + waitingRender = false + }) + waitingRender = true + } + } + + // Add window event listeners + if (model.positioning.followCursor) { + window.addEventListener('pointermove', handleWindowPointerMove) + isFollowingCursor.current = true + } + + // Store refs for cleanup + sceneRef.current = { + ...sceneRef.current!, + scene, + camera, + renderer, + controls, + dispose () { + if (!model.continiousRender) { + controls.removeEventListener('change', render) + } + if (model.positioning.followCursor) { + window.removeEventListener('pointermove', handleWindowPointerMove) + } + + // Clean up loaded models + for (const [modelUrl, model] of loadedModels.current) { + scene.remove(model) + model.traverse((child) => { + if (child instanceof THREE.Mesh) { + if (child.material) { + if (Array.isArray(child.material)) { + for (const mat of child.material) { + mat.dispose() + } + } else { + child.material.dispose() + } + } + if (child.geometry) { + child.geometry.dispose() + } + } + }) + } + loadedModels.current.clear() + modelLoaders.current.clear() + + const playerObject = sceneRef.current?.playerObject + if (playerObject?.skin.map) { + (playerObject.skin.map as unknown as THREE.Texture).dispose() + } + renderer.dispose() + renderer.domElement?.remove() + } + } + + return () => { + sceneRef.current?.dispose() + } + }, [model]) + + if (!model) return null + + const { x, y, width, height, scaled, onlyInitialScale } = model.positioning + const { windowWidth } = model.positioning + const { windowHeight } = model.positioning + const scaleValue = onlyInitialScale ? initialScale : 'var(--guiScale)' + + return ( +
    +
    +
    +
    +
    + ) +} diff --git a/src/reactUi.tsx b/src/reactUi.tsx index 1e6f13eb..6339686e 100644 --- a/src/reactUi.tsx +++ b/src/reactUi.tsx @@ -68,6 +68,7 @@ import FullscreenTime from './react/FullscreenTime' import StorageConflictModal from './react/StorageConflictModal' import FireRenderer from './react/FireRenderer' import MonacoEditor from './react/MonacoEditor' +import OverlayModelViewer from './react/OverlayModelViewer' const isFirefox = ua.getBrowser().name === 'Firefox' if (isFirefox) { @@ -259,6 +260,7 @@ const App = () => {
    + From 739a6fad24478c5b13b03f96babd853c569dcbbc Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Mon, 8 Sep 2025 04:34:17 +0300 Subject: [PATCH 163/181] fix lockfile --- pnpm-lock.yaml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fd8be62b..516370ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -140,7 +140,7 @@ importers: version: 4.17.21 mcraft-fun-mineflayer: specifier: ^0.1.23 - version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/4493c8ccbd6f7e775d76149d838f4386ae959f0e(encoding@0.1.13)) + version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13)) minecraft-data: specifier: 3.92.0 version: 3.92.0 @@ -345,7 +345,7 @@ importers: version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/89c33d396f3fde4804c71f4be3c203ade1833b41(@types/react@18.3.18)(react@18.3.1) mineflayer: specifier: github:zardoy/mineflayer#gen-the-master - version: https://codeload.github.com/zardoy/mineflayer/tar.gz/4493c8ccbd6f7e775d76149d838f4386ae959f0e(encoding@0.1.13) + version: https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13) mineflayer-mouse: specifier: ^0.1.21 version: 0.1.21 @@ -6670,7 +6670,7 @@ packages: minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9: resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9} - version: 1.61.0 + version: 1.62.0 engines: {node: '>=22'} minecraft-wrap@1.6.0: @@ -6688,8 +6688,8 @@ packages: resolution: {integrity: sha512-1XTVuw3twIrEcqQ1QRSB8NcStIUEZ+tbxiAG6rOrN/9M4thhtlS5PTJzFdmdrcYgWEBLvuOdJszaKE5zFfiXhg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/4493c8ccbd6f7e775d76149d838f4386ae959f0e: - resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/4493c8ccbd6f7e775d76149d838f4386ae959f0e} + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659: + resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659} version: 8.0.0 engines: {node: '>=22'} @@ -11344,7 +11344,7 @@ snapshots: dependencies: '@nxg-org/mineflayer-util-plugin': 1.8.4 minecraft-data: 3.92.0 - mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/4493c8ccbd6f7e775d76149d838f4386ae959f0e(encoding@0.1.13) + mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-item: 1.17.0 prismarine-physics: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b @@ -16990,12 +16990,12 @@ snapshots: dependencies: minecraft-data: 3.92.0 - mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/4493c8ccbd6f7e775d76149d838f4386ae959f0e(encoding@0.1.13)): + mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13)): dependencies: '@zardoy/flying-squid': 0.0.49(encoding@0.1.13) exit-hook: 2.2.1 minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13) - mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/4493c8ccbd6f7e775d76149d838f4386ae959f0e(encoding@0.1.13) + mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13) prismarine-item: 1.17.0 ws: 8.18.1 transitivePeerDependencies: @@ -17365,7 +17365,7 @@ snapshots: mineflayer-item-map-downloader@https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad)(encoding@0.1.13): dependencies: - mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/4493c8ccbd6f7e775d76149d838f4386ae959f0e(encoding@0.1.13) + mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13) sharp: 0.30.7 transitivePeerDependencies: - encoding @@ -17380,7 +17380,7 @@ snapshots: transitivePeerDependencies: - supports-color - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/4493c8ccbd6f7e775d76149d838f4386ae959f0e(encoding@0.1.13): + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13): dependencies: '@nxg-org/mineflayer-physics-util': 1.8.10 minecraft-data: 3.92.0 From 0b1183f541bb76a47ed15cfd63ef59340f91be9f Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Mon, 8 Sep 2025 04:36:09 +0300 Subject: [PATCH 164/181] up minecraft-data --- package.json | 4 ++-- scripts/makeOptimizedMcData.mjs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b5f66bfb..ff673726 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "jszip": "^3.10.1", "lodash-es": "^4.17.21", "mcraft-fun-mineflayer": "^0.1.23", - "minecraft-data": "3.92.0", + "minecraft-data": "3.98.0", "minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master", "mineflayer-item-map-downloader": "github:zardoy/mineflayer-item-map-downloader", "mojangson": "^2.0.4", @@ -205,7 +205,7 @@ "diamond-square": "github:zardoy/diamond-square", "prismarine-block": "github:zardoy/prismarine-block#next-era", "prismarine-world": "github:zardoy/prismarine-world#next-era", - "minecraft-data": "3.92.0", + "minecraft-data": "3.98.0", "prismarine-provider-anvil": "github:zardoy/prismarine-provider-anvil#everything", "prismarine-physics": "github:zardoy/prismarine-physics", "minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master", diff --git a/scripts/makeOptimizedMcData.mjs b/scripts/makeOptimizedMcData.mjs index 76e0f1c2..a572d067 100644 --- a/scripts/makeOptimizedMcData.mjs +++ b/scripts/makeOptimizedMcData.mjs @@ -371,6 +371,7 @@ console.log('size', fs.lstatSync(filePath).size / 1000 / 1000, gzipSizeFromFileS const { defaultVersion } = MCProtocol const data = MinecraftData(defaultVersion) +console.log('defaultVersion', defaultVersion, !!data) const initialMcData = { [defaultVersion]: { version: data.version, From f24cb49a8728a63cab19b8cc76fdbfb1888566eb Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Mon, 8 Sep 2025 04:55:43 +0300 Subject: [PATCH 165/181] up lockfile --- pnpm-lock.yaml | 74 +++++++++++++++++++++++++------------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 516370ce..5bcd74a0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,7 @@ overrides: diamond-square: github:zardoy/diamond-square prismarine-block: github:zardoy/prismarine-block#next-era prismarine-world: github:zardoy/prismarine-world#next-era - minecraft-data: 3.92.0 + minecraft-data: 3.98.0 prismarine-provider-anvil: github:zardoy/prismarine-provider-anvil#everything prismarine-physics: github:zardoy/prismarine-physics minecraft-protocol: github:PrismarineJS/node-minecraft-protocol#master @@ -142,8 +142,8 @@ importers: specifier: ^0.1.23 version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13)) minecraft-data: - specifier: 3.92.0 - version: 3.92.0 + specifier: 3.98.0 + version: 3.98.0 minecraft-protocol: specifier: github:PrismarineJS/node-minecraft-protocol#master version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13) @@ -170,7 +170,7 @@ importers: version: 6.1.1 prismarine-provider-anvil: specifier: github:zardoy/prismarine-provider-anvil#everything - version: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.92.0) + version: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.98.0) prosemirror-example-setup: specifier: ^1.2.2 version: 1.2.3 @@ -436,7 +436,7 @@ importers: version: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chunk: specifier: github:zardoy/prismarine-chunk#master - version: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0) + version: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.98.0) prismarine-schematic: specifier: ^1.2.0 version: 1.2.3 @@ -6448,7 +6448,7 @@ packages: resolution: {integrity: sha512-H9jPt2xEU77itC27dSz3qazHYqN9qVsv4HgMPozg7RqQ1uwgXmEa+ojKIlDtXf/TLJsG6Kv4EbzGa8a1Wh72uA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} peerDependencies: - minecraft-data: 3.92.0 + minecraft-data: 3.98.0 mcraft-fun-mineflayer@0.1.23: resolution: {integrity: sha512-qmI1rQQ0Ro5zJdi99rClWLF+mS9JZffgNX2vyWWesS3Hsk3Xblp/8swYTJKHSaFpNgzkVfXV92fEIrBqeH6iKA==} @@ -6658,8 +6658,8 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} - minecraft-data@3.92.0: - resolution: {integrity: sha512-CGfO50svzm+pSRa4Mbq4owsmRKbPCNkSZ3MCOyH+epC7yNjh+PUhPQFHWq72O51qsY7pAB5qM/bJn1ncwG1J5g==} + minecraft-data@3.98.0: + resolution: {integrity: sha512-JAPqJ/TZoxMUlAPPdWUh1v5wdqvYGFSZ4rW9bUtmaKBkGpomDSjw4V02ocBqbxKJvcTtmc5nM/LfN9/0DDqHrQ==} minecraft-folder-path@1.2.0: resolution: {integrity: sha512-qaUSbKWoOsH9brn0JQuBhxNAzTDMwrOXorwuRxdJKKKDYvZhtml+6GVCUrY5HRiEsieBEjCUnhVpDuQiKsiFaw==} @@ -7387,7 +7387,7 @@ packages: prismarine-biome@1.3.0: resolution: {integrity: sha512-GY6nZxq93mTErT7jD7jt8YS1aPrOakbJHh39seYsJFXvueIOdHAmW16kYQVrTVMW5MlWLQVxV/EquRwOgr4MnQ==} peerDependencies: - minecraft-data: 3.92.0 + minecraft-data: 3.98.0 prismarine-registry: ^1.1.0 prismarine-block@https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9: @@ -11343,7 +11343,7 @@ snapshots: '@nxg-org/mineflayer-trajectories@1.2.0(encoding@0.1.13)': dependencies: '@nxg-org/mineflayer-util-plugin': 1.8.4 - minecraft-data: 3.92.0 + minecraft-data: 3.98.0 mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-item: 1.17.0 @@ -13104,18 +13104,18 @@ snapshots: exit-hook: 2.2.1 flatmap: 0.0.3 long: 5.3.1 - mc-bridge: 0.1.3(minecraft-data@3.92.0) - minecraft-data: 3.92.0 + mc-bridge: 0.1.3(minecraft-data@3.98.0) + minecraft-data: 3.98.0 minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 - prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0) + prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.98.0) prismarine-entity: 2.5.0 prismarine-item: 1.17.0 prismarine-nbt: 2.7.0 - prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.92.0) + prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.98.0) prismarine-windows: 2.9.0 prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c rambda: 9.4.2 @@ -13142,16 +13142,16 @@ snapshots: exit-hook: 2.2.1 flatmap: 0.0.3 long: 5.3.1 - minecraft-data: 3.92.0 + minecraft-data: 3.98.0 minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13) 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/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0) + prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.98.0) prismarine-entity: 2.5.0 prismarine-item: 1.17.0 prismarine-nbt: 2.7.0 - prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.92.0) + prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.98.0) prismarine-windows: 2.9.0 prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c rambda: 9.4.2 @@ -14542,8 +14542,8 @@ snapshots: diamond-square@https://codeload.github.com/zardoy/diamond-square/tar.gz/cfaad2d1d5909fdfa63c8cc7bc05fb5e87782d71: dependencies: - minecraft-data: 3.92.0 - prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0) + minecraft-data: 3.98.0 + prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.98.0) prismarine-registry: 1.11.0 random-seed: 0.3.0 vec3: 0.1.10 @@ -16986,9 +16986,9 @@ snapshots: maxrects-packer: '@zardoy/maxrects-packer@2.7.4' zod: 3.24.2 - mc-bridge@0.1.3(minecraft-data@3.92.0): + mc-bridge@0.1.3(minecraft-data@3.98.0): dependencies: - minecraft-data: 3.92.0 + minecraft-data: 3.98.0 mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13)): dependencies: @@ -17302,7 +17302,7 @@ snapshots: min-indent@1.0.1: {} - minecraft-data@3.92.0: {} + minecraft-data@3.98.0: {} minecraft-folder-path@1.2.0: {} @@ -17322,7 +17322,7 @@ snapshots: debug: 4.4.0(supports-color@8.1.1) endian-toggle: 0.0.0 lodash.merge: 4.6.2 - minecraft-data: 3.92.0 + minecraft-data: 3.98.0 minecraft-folder-path: 1.2.0 node-fetch: 2.7.0(encoding@0.1.13) node-rsa: 0.4.2 @@ -17383,12 +17383,12 @@ snapshots: mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13): dependencies: '@nxg-org/mineflayer-physics-util': 1.8.10 - minecraft-data: 3.92.0 + minecraft-data: 3.98.0 minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13) - prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) + prismarine-biome: 1.3.0(minecraft-data@3.98.0)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chat: 1.11.0 - prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0) + prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.98.0) prismarine-entity: 2.5.0 prismarine-item: 1.17.0 prismarine-nbt: 2.7.0 @@ -18174,15 +18174,15 @@ snapshots: transitivePeerDependencies: - supports-color - prismarine-biome@1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0): + prismarine-biome@1.3.0(minecraft-data@3.98.0)(prismarine-registry@1.11.0): dependencies: - minecraft-data: 3.92.0 + minecraft-data: 3.98.0 prismarine-registry: 1.11.0 prismarine-block@https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9: dependencies: - minecraft-data: 3.92.0 - prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) + minecraft-data: 3.98.0 + prismarine-biome: 1.3.0(minecraft-data@3.98.0)(prismarine-registry@1.11.0) prismarine-chat: 1.11.0 prismarine-item: 1.17.0 prismarine-nbt: 2.7.0 @@ -18194,9 +18194,9 @@ snapshots: prismarine-nbt: 2.7.0 prismarine-registry: 1.11.0 - prismarine-chunk@https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0): + prismarine-chunk@https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.98.0): dependencies: - prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0) + prismarine-biome: 1.3.0(minecraft-data@3.98.0)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-nbt: 2.7.0 prismarine-registry: 1.11.0 @@ -18225,14 +18225,14 @@ snapshots: prismarine-physics@https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b: dependencies: - minecraft-data: 3.92.0 + minecraft-data: 3.98.0 prismarine-nbt: 2.7.0 vec3: 0.1.10 - prismarine-provider-anvil@https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.92.0): + prismarine-provider-anvil@https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.98.0): dependencies: prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 - prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0) + prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.98.0) prismarine-nbt: 2.7.0 prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c uint4: 0.1.2 @@ -18254,13 +18254,13 @@ snapshots: prismarine-registry@1.11.0: dependencies: - minecraft-data: 3.92.0 + minecraft-data: 3.98.0 prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-nbt: 2.7.0 prismarine-schematic@1.2.3: dependencies: - minecraft-data: 3.92.0 + minecraft-data: 3.98.0 prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-nbt: 2.7.0 prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c From 1525fac2a192f1fe7e2f858480c444948bbe9651 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Mon, 8 Sep 2025 05:22:24 +0300 Subject: [PATCH 166/181] fix: some visual camera world view issues (visible lines between blocks) --- renderer/viewer/three/cameraShake.ts | 25 ++++++++++++++++++++-- renderer/viewer/three/world/cursorBlock.ts | 18 ++++++++++++---- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/renderer/viewer/three/cameraShake.ts b/renderer/viewer/three/cameraShake.ts index 593b4628..7b159509 100644 --- a/renderer/viewer/three/cameraShake.ts +++ b/renderer/viewer/three/cameraShake.ts @@ -80,8 +80,12 @@ export class CameraShake { camera.setRotationFromQuaternion(yawQuat) } else { // For regular camera, apply all rotations - const pitchQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), this.basePitch) - const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), this.baseYaw) + // Add tiny offsets to prevent z-fighting at ideal angles (90, 180, 270 degrees) + const pitchOffset = this.addAntiZfightingOffset(this.basePitch) + const yawOffset = this.addAntiZfightingOffset(this.baseYaw) + + const pitchQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), pitchOffset) + const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), yawOffset) const rollQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), THREE.MathUtils.degToRad(this.rollAngle)) // Combine rotations in the correct order: pitch -> yaw -> roll const finalQuat = yawQuat.multiply(pitchQuat).multiply(rollQuat) @@ -96,4 +100,21 @@ export class CameraShake { private easeInOut (t: number): number { return t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2 } + + private addAntiZfightingOffset (angle: number): number { + const offset = 0.001 // Very small offset in radians (about 0.057 degrees) + + // Check if the angle is close to ideal angles (0, π/2, π, 3π/2) + const normalizedAngle = ((angle % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2) + const tolerance = 0.01 // Tolerance for considering an angle "ideal" + + if (Math.abs(normalizedAngle) < tolerance || + Math.abs(normalizedAngle - Math.PI / 2) < tolerance || + Math.abs(normalizedAngle - Math.PI) < tolerance || + Math.abs(normalizedAngle - 3 * Math.PI / 2) < tolerance) { + return angle + offset + } + + return angle + } } diff --git a/renderer/viewer/three/world/cursorBlock.ts b/renderer/viewer/three/world/cursorBlock.ts index b71c1b8d..a03a6999 100644 --- a/renderer/viewer/three/world/cursorBlock.ts +++ b/renderer/viewer/three/world/cursorBlock.ts @@ -28,7 +28,7 @@ export class CursorBlock { } cursorLineMaterial: LineMaterial - interactionLines: null | { blockPos: Vec3, mesh: THREE.Group } = null + interactionLines: null | { blockPos: Vec3, mesh: THREE.Group, shapePositions: BlocksShapes | undefined } = null prevColor: string | undefined blockBreakMesh: THREE.Mesh breakTextures: THREE.Texture[] = [] @@ -62,6 +62,13 @@ export class CursorBlock { this.worldRenderer.onReactivePlayerStateUpdated('gameMode', () => { this.updateLineMaterial() }) + // todo figure out why otherwise fog from skybox breaks it + setTimeout(() => { + this.updateLineMaterial() + if (this.interactionLines) { + this.setHighlightCursorBlock(this.interactionLines.blockPos, this.interactionLines.shapePositions, true) + } + }) } // Update functions @@ -69,6 +76,9 @@ export class CursorBlock { const inCreative = this.worldRenderer.playerStateReactive.gameMode === 'creative' const pixelRatio = this.worldRenderer.renderer.getPixelRatio() + if (this.cursorLineMaterial) { + this.cursorLineMaterial.dispose() + } this.cursorLineMaterial = new LineMaterial({ color: (() => { switch (this.worldRenderer.worldRendererConfig.highlightBlockColor) { @@ -115,8 +125,8 @@ export class CursorBlock { } } - setHighlightCursorBlock (blockPos: Vec3 | null, shapePositions?: BlocksShapes): void { - if (blockPos && this.interactionLines && blockPos.equals(this.interactionLines.blockPos)) { + setHighlightCursorBlock (blockPos: Vec3 | null, shapePositions?: BlocksShapes, force = false): void { + if (blockPos && this.interactionLines && blockPos.equals(this.interactionLines.blockPos) && !force) { return } if (this.interactionLines !== null) { @@ -140,7 +150,7 @@ export class CursorBlock { } this.worldRenderer.scene.add(group) group.visible = !this.cursorLinesHidden - this.interactionLines = { blockPos, mesh: group } + this.interactionLines = { blockPos, mesh: group, shapePositions } } render () { From c4097975bf8ea14764bf7f2b70aa00e45dc5b399 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Mon, 8 Sep 2025 05:29:34 +0300 Subject: [PATCH 167/181] add a way to disable sky box for old behavior (not tested) --- renderer/viewer/lib/worldrendererCommon.ts | 1 + renderer/viewer/three/skyboxRenderer.ts | 25 ++++++++++++++++++++- renderer/viewer/three/worldrendererThree.ts | 5 ++++- src/defaultOptions.ts | 1 + src/watchOptions.ts | 4 ++++ 5 files changed, 34 insertions(+), 2 deletions(-) diff --git a/renderer/viewer/lib/worldrendererCommon.ts b/renderer/viewer/lib/worldrendererCommon.ts index e2455915..4140e3fa 100644 --- a/renderer/viewer/lib/worldrendererCommon.ts +++ b/renderer/viewer/lib/worldrendererCommon.ts @@ -47,6 +47,7 @@ export const defaultWorldRendererConfig = { smoothLighting: true, enableLighting: true, starfield: true, + defaultSkybox: true, renderEntities: true, extraBlockRenderers: true, foreground: true, diff --git a/renderer/viewer/three/skyboxRenderer.ts b/renderer/viewer/three/skyboxRenderer.ts index aa8c3bb6..cd7bd879 100644 --- a/renderer/viewer/three/skyboxRenderer.ts +++ b/renderer/viewer/three/skyboxRenderer.ts @@ -18,7 +18,7 @@ export class SkyboxRenderer { private fogBrightness = 0 private prevFogBrightness = 0 - constructor (private readonly scene: THREE.Scene, public initialImage: string | null) { + constructor (private readonly scene: THREE.Scene, public defaultSkybox: boolean, public initialImage: string | null) { if (!initialImage) { this.createGradientSky() } @@ -119,6 +119,12 @@ export class SkyboxRenderer { this.updateSkyColors() } + // Update default skybox setting + updateDefaultSkybox (defaultSkybox: boolean) { + this.defaultSkybox = defaultSkybox + this.updateSkyColors() + } + private createGradientSky () { const size = 64 const scale = 256 / size + 2 @@ -279,6 +285,23 @@ export class SkyboxRenderer { private updateSkyColors () { if (!this.skyMesh || !this.voidMesh) return + // If default skybox is disabled, hide the skybox meshes + if (!this.defaultSkybox) { + this.skyMesh.visible = false + this.voidMesh.visible = false + if (this.mesh) { + this.mesh.visible = false + } + return + } + + // Show skybox meshes when default skybox is enabled + this.skyMesh.visible = true + this.voidMesh.visible = true + if (this.mesh) { + this.mesh.visible = true + } + // Update fog brightness with smooth transition this.prevFogBrightness = this.fogBrightness const renderDistance = this.viewDistance / 32 diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index 29e9223c..1b4e6152 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -98,7 +98,7 @@ export class WorldRendererThree extends WorldRendererCommon { this.holdingBlockLeft = new HoldingBlock(this, true) // Initialize skybox renderer - this.skyboxRenderer = new SkyboxRenderer(this.scene, null) + this.skyboxRenderer = new SkyboxRenderer(this.scene, this.worldRendererConfig.defaultSkybox, null) void this.skyboxRenderer.init() this.addDebugOverlay() @@ -206,6 +206,9 @@ export class WorldRendererThree extends WorldRendererCommon { this.onReactiveConfigUpdated('showChunkBorders', (value) => { this.updateShowChunksBorder(value) }) + this.onReactiveConfigUpdated('defaultSkybox', (value) => { + this.skyboxRenderer.updateDefaultSkybox(value) + }) } changeHandSwingingState (isAnimationPlaying: boolean, isLeft = false) { diff --git a/src/defaultOptions.ts b/src/defaultOptions.ts index 6045e70b..361879be 100644 --- a/src/defaultOptions.ts +++ b/src/defaultOptions.ts @@ -41,6 +41,7 @@ export const defaultOptions = { renderEars: true, lowMemoryMode: false, starfieldRendering: true, + defaultSkybox: true, enabledResourcepack: null as string | null, useVersionsTextures: 'latest', serverResourcePacks: 'prompt' as 'prompt' | 'always' | 'never', diff --git a/src/watchOptions.ts b/src/watchOptions.ts index da75cc74..779aa29f 100644 --- a/src/watchOptions.ts +++ b/src/watchOptions.ts @@ -116,6 +116,10 @@ export const watchOptionsAfterViewerInit = () => { appViewer.inWorldRenderingConfig.starfield = o.starfieldRendering }) + watchValue(options, o => { + appViewer.inWorldRenderingConfig.defaultSkybox = o.defaultSkybox + }) + watchValue(options, o => { // appViewer.inWorldRenderingConfig.neighborChunkUpdates = o.neighborChunkUpdates }) From 06dc3cb033129f925dd492b01a809e9e5f3cd19c Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Mon, 8 Sep 2025 05:38:16 +0300 Subject: [PATCH 168/181] feat: Add saveLoginPassword option to control password saving behavior in browser for offline auth on servers --- src/defaultOptions.ts | 1 + src/optionsGuiScheme.tsx | 10 ++++++++++ src/react/ChatProvider.tsx | 20 ++++++++++++++++---- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/defaultOptions.ts b/src/defaultOptions.ts index 361879be..85ebae17 100644 --- a/src/defaultOptions.ts +++ b/src/defaultOptions.ts @@ -84,6 +84,7 @@ export const defaultOptions = { localServerOptions: { gameMode: 1 } as any, + saveLoginPassword: 'prompt' as 'prompt' | 'never' | 'always', preferLoadReadonly: false, experimentalClientSelfReload: false, remoteSoundsSupport: false, diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx index b03db37d..a47c06eb 100644 --- a/src/optionsGuiScheme.tsx +++ b/src/optionsGuiScheme.tsx @@ -550,6 +550,16 @@ export const guiOptionsScheme: { return Server Connection }, }, + { + saveLoginPassword: { + tooltip: 'Controls whether to save login passwords for servers in this browser memory.', + values: [ + 'prompt', + 'always', + 'never' + ] + }, + }, { custom () { const { serversAutoVersionSelect } = useSnapshot(options) diff --git a/src/react/ChatProvider.tsx b/src/react/ChatProvider.tsx index 0bb13285..066bc48a 100644 --- a/src/react/ChatProvider.tsx +++ b/src/react/ChatProvider.tsx @@ -73,16 +73,28 @@ export default () => { } const builtinHandled = tryHandleBuiltinCommand(message) - if (getServerIndex() !== undefined && (message.startsWith('/login') || message.startsWith('/register'))) { - showNotification('Click here to save your password in browser for auto-login', undefined, false, undefined, () => { + if (getServerIndex() !== undefined && (message.startsWith('/login') || message.startsWith('/register')) && options.saveLoginPassword !== 'never') { + const savePassword = () => { + let hadPassword = false updateLoadedServerData((server) => { server.autoLogin ??= {} const password = message.split(' ')[1] + hadPassword = !!server.autoLogin[bot.username] server.autoLogin[bot.username] = password return { ...server } }) - hideNotification() - }) + if (options.saveLoginPassword === 'always') { + const message = hadPassword ? 'Password updated in browser for auto-login' : 'Password saved in browser for auto-login' + showNotification(message, undefined, false, undefined) + } else { + hideNotification() + } + } + if (options.saveLoginPassword === 'prompt') { + showNotification('Click here to save your password in browser for auto-login', undefined, false, undefined, savePassword) + } else { + savePassword() + } notificationProxy.id = 'auto-login' const listener = () => { hideNotification() From 852dd737aefc045a1d4193d17803d10a0cce1b48 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Thu, 11 Sep 2025 22:24:04 +0300 Subject: [PATCH 169/181] fix: fix some UI like error screen was not visible fully (buttons were clipped behind the screen) on larger scale on large screens --- src/screens.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/screens.css b/src/screens.css index f0040e2d..e503c305 100644 --- a/src/screens.css +++ b/src/screens.css @@ -26,6 +26,10 @@ display: flex; justify-content: center; z-index: 12; + /* Account for GUI scaling */ + width: calc(100dvw / var(--guiScale, 1)); + height: calc(100dvh / var(--guiScale, 1)); + overflow: hidden; } .screen-content { From c930365e329aef0c44845ab06aa42266f5a6a16e Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Thu, 18 Sep 2025 07:49:44 +0300 Subject: [PATCH 170/181] fix sometimes inventory player should not be rendered --- src/inventoryWindows.ts | 2 ++ src/react/OverlayModelViewer.tsx | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/src/inventoryWindows.ts b/src/inventoryWindows.ts index d16fee20..d40260df 100644 --- a/src/inventoryWindows.ts +++ b/src/inventoryWindows.ts @@ -470,6 +470,7 @@ const openWindow = (type: string | undefined, title: string | any = undefined) = const isRightClick = type === 'rightclick' const isLeftClick = type === 'leftclick' if (isLeftClick || isRightClick) { + modelViewerState.model = undefined inv.canvasManager.children[0].showRecipesOrUsages(isLeftClick, item) } } else { @@ -501,6 +502,7 @@ const openWindow = (type: string | undefined, title: string | any = undefined) = if (freeSlot === null) return void bot.creative.setInventorySlot(freeSlot, item) } else { + modelViewerState.model = undefined inv.canvasManager.children[0].showRecipesOrUsages(!isRightclick, mapSlots([item], true)[0]) } } diff --git a/src/react/OverlayModelViewer.tsx b/src/react/OverlayModelViewer.tsx index 24dc836d..0fdeae75 100644 --- a/src/react/OverlayModelViewer.tsx +++ b/src/react/OverlayModelViewer.tsx @@ -6,6 +6,7 @@ import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader' import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader' import { applySkinToPlayerObject, createPlayerObject, PlayerObjectType } from '../../renderer/viewer/lib/createPlayerObject' import { currentScaling } from '../scaleInterface' +import { activeModalStack } from '../globalState' THREE.ColorManagement.enabled = false @@ -29,6 +30,7 @@ export const modelViewerState = proxy({ modelCustomization?: { [modelUrl: string]: { color?: string, opacity?: number, metalness?: number, roughness?: number } } resetRotationOnReleae?: boolean continiousRender?: boolean + alwaysRender?: boolean } }) globalThis.modelViewerState = modelViewerState @@ -75,6 +77,15 @@ globalThis.getModelViewerValues = () => { } } +subscribe(activeModalStack, () => { + if (!modelViewerState.model || !modelViewerState.model?.alwaysRender) { + return + } + if (activeModalStack.length === 0) { + modelViewerState.model = undefined + } +}) + export default () => { const { model } = useSnapshot(modelViewerState) const containerRef = useRef(null) From 636a7fdb54a2fec622b5b31da4691a632049b5f0 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Fri, 19 Sep 2025 04:42:22 +0200 Subject: [PATCH 171/181] feat: improve fog a little (#427) --- renderer/viewer/three/skyboxRenderer.ts | 43 ++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/renderer/viewer/three/skyboxRenderer.ts b/renderer/viewer/three/skyboxRenderer.ts index cd7bd879..fb9edae6 100644 --- a/renderer/viewer/three/skyboxRenderer.ts +++ b/renderer/viewer/three/skyboxRenderer.ts @@ -1,4 +1,5 @@ import * as THREE from 'three' +import { DebugGui } from '../lib/DebugGui' export const DEFAULT_TEMPERATURE = 0.75 @@ -17,11 +18,33 @@ export class SkyboxRenderer { private waterBreathing = false private fogBrightness = 0 private prevFogBrightness = 0 + private readonly fogOrangeness = 0 // Debug property to control sky color orangeness + private readonly distanceFactor = 2.7 + + private readonly brightnessAtPosition = 1 + debugGui: DebugGui constructor (private readonly scene: THREE.Scene, public defaultSkybox: boolean, public initialImage: string | null) { + this.debugGui = new DebugGui('skybox_renderer', this, [ + 'temperature', + 'worldTime', + 'inWater', + 'waterBreathing', + 'fogOrangeness', + 'brightnessAtPosition', + 'distanceFactor' + ], { + brightnessAtPosition: { min: 0, max: 1, step: 0.01 }, + temperature: { min: 0, max: 1, step: 0.01 }, + worldTime: { min: 0, max: 24_000, step: 1 }, + fogOrangeness: { min: -1, max: 1, step: 0.01 }, + distanceFactor: { min: 0, max: 5, step: 0.01 }, + }) + if (!initialImage) { this.createGradientSky() } + // this.debugGui.activate() } async init () { @@ -95,6 +118,7 @@ export class SkyboxRenderer { // Update world time updateTime (timeOfDay: number, partialTicks = 0) { + if (this.debugGui.visible) return this.worldTime = timeOfDay this.partialTicks = partialTicks this.updateSkyColors() @@ -108,12 +132,14 @@ export class SkyboxRenderer { // Update temperature (for biome support) updateTemperature (temperature: number) { + if (this.debugGui.visible) return this.temperature = temperature this.updateSkyColors() } // Update water state updateWaterState (inWater: boolean, waterBreathing: boolean) { + if (this.debugGui.visible) return this.inWater = inWater this.waterBreathing = waterBreathing this.updateSkyColors() @@ -121,6 +147,7 @@ export class SkyboxRenderer { // Update default skybox setting updateDefaultSkybox (defaultSkybox: boolean) { + if (this.debugGui.visible) return this.defaultSkybox = defaultSkybox this.updateSkyColors() } @@ -229,8 +256,15 @@ export class SkyboxRenderer { if (temperature < -1) temperature = -1 if (temperature > 1) temperature = 1 - const hue = 0.622_222_2 - temperature * 0.05 - const saturation = 0.5 + temperature * 0.1 + // Apply debug fog orangeness to hue - positive values make it more orange, negative make it less orange + const baseHue = 0.622_222_2 - temperature * 0.05 + // Orange is around hue 0.08-0.15, so we need to shift from blue-purple (0.62) toward orange + // Use a more dramatic shift and also increase saturation for more noticeable effect + const orangeHue = 0.12 // Orange hue value + const hue = this.fogOrangeness > 0 + ? baseHue + (orangeHue - baseHue) * this.fogOrangeness * 0.8 // Blend toward orange + : baseHue + this.fogOrangeness * 0.1 // Subtle shift for negative values + const saturation = 0.5 + temperature * 0.1 + Math.abs(this.fogOrangeness) * 0.3 // Increase saturation with orangeness const brightness = 1 return this.hsbToRgb(hue, saturation, brightness) @@ -305,8 +339,7 @@ export class SkyboxRenderer { // Update fog brightness with smooth transition this.prevFogBrightness = this.fogBrightness const renderDistance = this.viewDistance / 32 - const brightnessAtPosition = 1 // Could be affected by light level in future - const targetBrightness = brightnessAtPosition * (1 - renderDistance) + renderDistance + const targetBrightness = this.brightnessAtPosition * (1 - renderDistance) + renderDistance this.fogBrightness += (targetBrightness - this.fogBrightness) * 0.1 // Handle water fog @@ -340,7 +373,7 @@ export class SkyboxRenderer { const blue = (fogColor.z + (skyColor.z - fogColor.z) * viewFactor) * clampedBrightness * interpolatedBrightness this.scene.background = new THREE.Color(red, green, blue) - this.scene.fog = new THREE.Fog(new THREE.Color(red, green, blue), 0.0025, viewDistance * 2) + this.scene.fog = new THREE.Fog(new THREE.Color(red, green, blue), 0.0025, viewDistance * this.distanceFactor) ;(this.skyMesh.material as THREE.MeshBasicMaterial).color.set(new THREE.Color(skyColor.x, skyColor.y, skyColor.z)) ;(this.voidMesh.material as THREE.MeshBasicMaterial).color.set(new THREE.Color( From 3b94889bed40e9c687be52c5ca9a87172c6c6a9d Mon Sep 17 00:00:00 2001 From: Vitaly Date: Sat, 20 Sep 2025 01:57:59 +0200 Subject: [PATCH 172/181] feat: make arrows colorful and metadata (#430) Co-authored-by: Cursor Agent --- renderer/viewer/three/waypointSprite.ts | 36 ++++++++++++++++++++----- renderer/viewer/three/waypoints.ts | 4 ++- src/customChannels.ts | 17 +++++++++++- 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/renderer/viewer/three/waypointSprite.ts b/renderer/viewer/three/waypointSprite.ts index 7c8cf1f6..6a30e6db 100644 --- a/renderer/viewer/three/waypointSprite.ts +++ b/renderer/viewer/three/waypointSprite.ts @@ -16,7 +16,7 @@ export const WAYPOINT_CONFIG = { CANVAS_SCALE: 2, ARROW: { enabledDefault: false, - pixelSize: 30, + pixelSize: 50, paddingPx: 50, }, } @@ -50,6 +50,7 @@ export function createWaypointSprite (options: { depthTest?: boolean, // Y offset in world units used by updateScaleWorld only (screen-pixel API ignores this) labelYOffset?: number, + metadata?: any, }): WaypointSprite { const color = options.color ?? 0xFF_00_00 const depthTest = options.depthTest ?? false @@ -131,16 +132,22 @@ export function createWaypointSprite (options: { canvas.height = size const ctx = canvas.getContext('2d')! ctx.clearRect(0, 0, size, size) + + // Draw arrow shape ctx.beginPath() - ctx.moveTo(size * 0.2, size * 0.5) - ctx.lineTo(size * 0.8, size * 0.5) - ctx.lineTo(size * 0.5, size * 0.2) + ctx.moveTo(size * 0.15, size * 0.5) + ctx.lineTo(size * 0.85, size * 0.5) + ctx.lineTo(size * 0.5, size * 0.15) ctx.closePath() - ctx.lineWidth = 4 + + // Use waypoint color for arrow + const colorHex = `#${color.toString(16).padStart(6, '0')}` + ctx.lineWidth = 6 ctx.strokeStyle = 'black' ctx.stroke() - ctx.fillStyle = 'white' + ctx.fillStyle = colorHex ctx.fill() + const texture = new THREE.CanvasTexture(canvas) const material = new THREE.SpriteMaterial({ map: texture, transparent: true, depthTest: false, depthWrite: false }) arrowSprite = new THREE.Sprite(material) @@ -169,6 +176,9 @@ export function createWaypointSprite (options: { ensureArrow() if (!arrowSprite) return true + // Check if onlyLeftRight is enabled in metadata + const onlyLeftRight = options.metadata?.onlyLeftRight === true + // Build camera basis using camera.up to respect custom orientations const forward = new THREE.Vector3() camera.getWorldDirection(forward) // camera look direction @@ -213,6 +223,20 @@ export function createWaypointSprite (options: { } } + // Apply onlyLeftRight logic - restrict arrows to left/right edges only + if (onlyLeftRight) { + // Force the arrow to appear only on left or right edges + if (Math.abs(rx) > Math.abs(ry)) { + // Horizontal direction is dominant, keep it + ry = 0 + } else { + // Vertical direction is dominant, but we want only left/right + // So choose left or right based on the sign of rx + rx = rx >= 0 ? 1 : -1 + ry = 0 + } + } + // Place on the rectangle border [-1,1]x[-1,1] const s = Math.max(Math.abs(rx), Math.abs(ry)) || 1 let ndcX = rx / s diff --git a/renderer/viewer/three/waypoints.ts b/renderer/viewer/three/waypoints.ts index cebd779a..256ca6df 100644 --- a/renderer/viewer/three/waypoints.ts +++ b/renderer/viewer/three/waypoints.ts @@ -17,6 +17,7 @@ interface WaypointOptions { color?: number label?: string minDistance?: number + metadata?: any } export class WaypointsRenderer { @@ -71,13 +72,14 @@ export class WaypointsRenderer { this.removeWaypoint(id) const color = options.color ?? 0xFF_00_00 - const { label } = options + const { label, metadata } = options const minDistance = options.minDistance ?? 0 const sprite = createWaypointSprite({ position: new THREE.Vector3(x, y, z), color, label: (label || id), + metadata, }) sprite.enableOffscreenArrow(true) sprite.setArrowParent(this.waypointScene) diff --git a/src/customChannels.ts b/src/customChannels.ts index b566f9dd..506ea776 100644 --- a/src/customChannels.ts +++ b/src/customChannels.ts @@ -82,15 +82,30 @@ const registerWaypointChannels = () => { { name: 'color', type: 'i32' + }, + { + name: 'metadataJson', + type: ['pstring', { countType: 'i16' }] } ] ] registerChannel('minecraft-web-client:waypoint-add', packetStructure, (data) => { + // Parse metadata if provided + let metadata: any = {} + if (data.metadataJson && data.metadataJson.trim() !== '') { + try { + metadata = JSON.parse(data.metadataJson) + } catch (error) { + console.warn('Failed to parse waypoint metadataJson:', error) + } + } + getThreeJsRendererMethods()?.addWaypoint(data.id, data.x, data.y, data.z, { minDistance: data.minDistance, label: data.label || undefined, - color: data.color || undefined + color: data.color || undefined, + metadata }) }) From 4f421ae45fda892cc364cc60de47e4fc79799eee Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sun, 28 Sep 2025 21:59:00 +0300 Subject: [PATCH 173/181] respect loadPlayerSkins option for inventory skin --- src/watchOptions.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/watchOptions.ts b/src/watchOptions.ts index 779aa29f..de7d30d3 100644 --- a/src/watchOptions.ts +++ b/src/watchOptions.ts @@ -3,6 +3,7 @@ import { subscribeKey } from 'valtio/utils' import { isMobile } from 'renderer/viewer/lib/simpleUtils' import { WorldDataEmitter } from 'renderer/viewer/lib/worldDataEmitter' +import { setSkinsConfig } from 'renderer/viewer/lib/utils/skins' import { options, watchValue } from './optionsStorage' import { reloadChunks } from './utils' import { miscUiState } from './globalState' @@ -97,6 +98,8 @@ export const watchOptionsAfterViewerInit = () => { appViewer.inWorldRenderingConfig.highlightBlockColor = o.highlightBlockColor appViewer.inWorldRenderingConfig._experimentalSmoothChunkLoading = o.rendererSharedOptions._experimentalSmoothChunkLoading appViewer.inWorldRenderingConfig._renderByChunks = o.rendererSharedOptions._renderByChunks + + setSkinsConfig({ apiEnabled: o.loadPlayerSkins }) }) appViewer.inWorldRenderingConfig.smoothLighting = options.smoothLighting From b239636356c9bb828181cf069c3756c722cebd33 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sun, 28 Sep 2025 22:04:17 +0300 Subject: [PATCH 174/181] feat: add debugServerPacketNames and debugClientPacketNames for quick access of names with intellisense of packets for current protocol. Should be used with `window.inspectPacket` in console --- src/devtools.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/devtools.ts b/src/devtools.ts index 6c47f73d..1f8ef8e8 100644 --- a/src/devtools.ts +++ b/src/devtools.ts @@ -5,6 +5,17 @@ import { WorldRendererThree } from 'renderer/viewer/three/worldrendererThree' import { enable, disable, enabled } from 'debug' import { Vec3 } from 'vec3' +customEvents.on('mineflayerBotCreated', () => { + window.debugServerPacketNames = Object.fromEntries(Object.keys(loadedData.protocol.play.toClient.types).map(name => { + name = name.replace('packet_', '') + return [name, name] + })) + window.debugClientPacketNames = Object.fromEntries(Object.keys(loadedData.protocol.play.toServer.types).map(name => { + name = name.replace('packet_', '') + return [name, name] + })) +}) + window.Vec3 = Vec3 window.cursorBlockRel = (x = 0, y = 0, z = 0) => { const newPos = bot.blockAtCursor(5)?.position.offset(x, y, z) From 05cd560d6b67e287acd6684ffeacc0db4b0b2386 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Mon, 29 Sep 2025 02:01:04 +0300 Subject: [PATCH 175/181] add shadow and directional light for player in inventory (model viewer) --- src/react/OverlayModelViewer.tsx | 54 +++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/src/react/OverlayModelViewer.tsx b/src/react/OverlayModelViewer.tsx index 0fdeae75..e48a2f0b 100644 --- a/src/react/OverlayModelViewer.tsx +++ b/src/react/OverlayModelViewer.tsx @@ -119,11 +119,15 @@ export default () => { modelLoaders.current.set(modelUrl, loader) const onLoad = (object: THREE.Object3D) => { - // Apply customization if available + // Apply customization if available and enable shadows const customization = model?.modelCustomization?.[modelUrl] - if (customization) { - object.traverse((child) => { - if (child instanceof THREE.Mesh && child.material) { + object.traverse((child) => { + if (child instanceof THREE.Mesh) { + // Enable shadow casting and receiving for all meshes + child.castShadow = true + child.receiveShadow = true + + if (child.material && customization) { const material = child.material as THREE.MeshStandardMaterial if (customization.color) { material.color.setHex(parseInt(customization.color.replace('#', ''), 16)) @@ -139,8 +143,8 @@ export default () => { material.roughness = customization.roughness } } - }) - } + } + }) // Center and scale model const box = new THREE.Box3().setFromObject(object) @@ -259,6 +263,12 @@ export default () => { } renderer.setPixelRatio(scale) renderer.setSize(model.positioning.width, model.positioning.height) + + // Enable shadow rendering for depth and realism + renderer.shadowMap.enabled = true + renderer.shadowMap.type = THREE.PCFSoftShadowMap // Soft shadows for better quality + renderer.shadowMap.autoUpdate = true + containerRef.current.appendChild(renderer.domElement) // Setup controls @@ -270,10 +280,30 @@ export default () => { controls.enableDamping = true controls.dampingFactor = 0.05 - // Add ambient light - const ambientLight = new THREE.AmbientLight(0xff_ff_ff, 1) + // Add ambient light for overall illumination + const ambientLight = new THREE.AmbientLight(0xff_ff_ff, 0.4) // Reduced intensity to allow shadows scene.add(ambientLight) + // Add directional light for shadows and depth (similar to Minecraft inventory lighting) + const directionalLight = new THREE.DirectionalLight(0xff_ff_ff, 0.6) + directionalLight.position.set(2, 2, 2) // Position light from top-right-front + directionalLight.target.position.set(0, 0, 0) // Point towards center of scene + + // Configure shadow properties for optimal quality + directionalLight.castShadow = true + directionalLight.shadow.mapSize.width = 2048 // High resolution shadow map + directionalLight.shadow.mapSize.height = 2048 + directionalLight.shadow.camera.near = 0.1 + directionalLight.shadow.camera.far = 10 + directionalLight.shadow.camera.left = -3 + directionalLight.shadow.camera.right = 3 + directionalLight.shadow.camera.top = 3 + directionalLight.shadow.camera.bottom = -3 + directionalLight.shadow.bias = -0.0001 // Reduce shadow acne + + scene.add(directionalLight) + scene.add(directionalLight.target) + // Cursor following function const updatePlayerLookAt = () => { if (!isFollowingCursor.current || !sceneRef.current?.playerObject) return @@ -342,6 +372,14 @@ export default () => { scale: 1 // Start with base scale, will adjust below }) + // Enable shadows for player object + wrapper.traverse((child) => { + if (child instanceof THREE.Mesh) { + child.castShadow = true + child.receiveShadow = true + } + }) + // Calculate proper scale and positioning for camera view const box = new THREE.Box3().setFromObject(wrapper) const size = box.getSize(new THREE.Vector3()) From f51254d97a9a04be3eb5750d214c59e0c41ffe76 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Tue, 30 Sep 2025 07:20:30 +0300 Subject: [PATCH 176/181] fix: dont stop local replay server with keep alive connection error --- src/packetsReplay/replayPackets.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/packetsReplay/replayPackets.ts b/src/packetsReplay/replayPackets.ts index d0d95da8..54b3d652 100644 --- a/src/packetsReplay/replayPackets.ts +++ b/src/packetsReplay/replayPackets.ts @@ -59,6 +59,7 @@ export const startLocalReplayServer = (contents: string) => { const server = createServer({ Server: LocalServer as any, version: header.minecraftVersion, + keepAlive: false, 'online-mode': false }) From a88c8b547044c1dab9c759e56794d614cc41ffa4 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Tue, 30 Sep 2025 09:38:37 +0300 Subject: [PATCH 177/181] possible fix for rare edgecase where skins from server were not applied. Cause: renderer due to rare circumnstances could be loaded AFTER gameLoaded which is fired only when starting rendering 3d world. classic no existing data handling issue why not mineflayerBotCreated? because getThreeJsRendererMethods not available at that time so would make things only much complex --- src/entities.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/entities.ts b/src/entities.ts index dcec6143..674f91ef 100644 --- a/src/entities.ts +++ b/src/entities.ts @@ -246,22 +246,29 @@ customEvents.on('gameLoaded', () => { } } // even if not found, still record to cache - void getThreeJsRendererMethods()?.updatePlayerSkin(entityId, player.username, player.uuid, skinUrl ?? true, capeUrl) + void getThreeJsRendererMethods()!.updatePlayerSkin(entityId, player.username, player.uuid, skinUrl ?? true, capeUrl) } catch (err) { - console.error('Error decoding player texture:', err) + reportError(new Error('Error applying skin texture:', { cause: err })) } } bot.on('playerJoined', updateSkin) bot.on('playerUpdated', updateSkin) + for (const entity of Object.values(bot.players)) { + updateSkin(entity) + } - bot.on('teamUpdated', (team: Team) => { + const teamUpdated = (team: Team) => { for (const entity of Object.values(bot.entities)) { if (entity.type === 'player' && entity.username && team.members.includes(entity.username) || entity.uuid && team.members.includes(entity.uuid)) { bot.emit('entityUpdate', entity) } } - }) + } + bot.on('teamUpdated', teamUpdated) + for (const team of Object.values(bot.teams)) { + teamUpdated(team) + } const updateEntityNameTags = (team: Team) => { for (const entity of Object.values(bot.entities)) { From 634df8d03dfd90aa978433e39c23376a4116a15d Mon Sep 17 00:00:00 2001 From: Colbster937 Date: Fri, 10 Oct 2025 17:52:06 -0500 Subject: [PATCH 178/181] Add WebMC & WS changes (#431) Co-authored-by: Colbster937 <96893162+colbychittenden@users.noreply.github.com> --- config.json | 4 ++++ src/appConfig.ts | 2 +- src/mineflayer/websocket-core.ts | 7 +++++-- src/react/AddServerOrConnect.tsx | 2 +- src/react/ServersListProvider.tsx | 2 ++ 5 files changed, 13 insertions(+), 4 deletions(-) diff --git a/config.json b/config.json index 940fb738..2bfa9cfe 100644 --- a/config.json +++ b/config.json @@ -10,6 +10,10 @@ { "ip": "wss://play.mcraft.fun" }, + { + "ip": "wss://play.webmc.fun", + "name": "WebMC" + }, { "ip": "wss://ws.fuchsmc.net" }, diff --git a/src/appConfig.ts b/src/appConfig.ts index 92fde21a..c29d74e8 100644 --- a/src/appConfig.ts +++ b/src/appConfig.ts @@ -35,7 +35,7 @@ export type AppConfig = { // defaultVersion?: string peerJsServer?: string peerJsServerFallback?: string - promoteServers?: Array<{ ip, description, version? }> + promoteServers?: Array<{ ip, description, name?, version?, }> mapsProvider?: string appParams?: Record // query string params diff --git a/src/mineflayer/websocket-core.ts b/src/mineflayer/websocket-core.ts index 0edd2497..f8163102 100644 --- a/src/mineflayer/websocket-core.ts +++ b/src/mineflayer/websocket-core.ts @@ -15,9 +15,12 @@ class CustomDuplex extends Duplex { } export const getWebsocketStream = async (host: string) => { - const baseProtocol = location.protocol === 'https:' ? 'wss' : host.startsWith('ws://') ? 'ws' : 'wss' + const baseProtocol = host.startsWith('ws://') ? 'ws' : 'wss' const hostClean = host.replace('ws://', '').replace('wss://', '') - const ws = new WebSocket(`${baseProtocol}://${hostClean}`) + const hostURL = new URL(`${baseProtocol}://${hostClean}`) + const hostParams = hostURL.searchParams + hostParams.append('client_mcraft', '') + const ws = new WebSocket(`${baseProtocol}://${hostURL.host}${hostURL.pathname}?${hostParams.toString()}`) const clientDuplex = new CustomDuplex(undefined, data => { ws.send(data) }) diff --git a/src/react/AddServerOrConnect.tsx b/src/react/AddServerOrConnect.tsx index d478b3e7..36fd5264 100644 --- a/src/react/AddServerOrConnect.tsx +++ b/src/react/AddServerOrConnect.tsx @@ -117,7 +117,7 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ } const displayConnectButton = qsParamIp - const serverExamples = ['example.com:25565', 'play.hypixel.net', 'ws://play.pcm.gg'] + const serverExamples = ['example.com:25565', 'play.hypixel.net', 'ws://play.pcm.gg', 'wss://play.webmc.fun'] // pick random example const example = serverExamples[Math.floor(Math.random() * serverExamples.length)] diff --git a/src/react/ServersListProvider.tsx b/src/react/ServersListProvider.tsx index 75f95d3f..42ef2aaa 100644 --- a/src/react/ServersListProvider.tsx +++ b/src/react/ServersListProvider.tsx @@ -119,6 +119,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL ...serversListProvided, ...(customServersList ? [] : (miscUiState.appConfig?.promoteServers ?? [])).map((server): StoreServerItem => ({ ip: server.ip, + name: server.name, versionOverride: server.version, description: server.description, isRecommended: true @@ -167,6 +168,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL console.log('pingResult.fullInfo.description', pingResult.fullInfo.description) data = { formattedText: pingResult.fullInfo.description, + icon: pingResult.fullInfo.favicon, textNameRight: `ws ${pingResult.latency}ms`, textNameRightGrayed: `${pingResult.fullInfo.players?.online ?? '??'}/${pingResult.fullInfo.players?.max ?? '??'}`, offline: false From e9f91f8ecda1488c636f35f58cc522f459a29f82 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sat, 11 Oct 2025 02:24:51 +0300 Subject: [PATCH 179/181] feat: enable music by default, add slider for controlling its volume --- src/basicSounds.ts | 30 ++++++++++++++++++++---------- src/defaultOptions.ts | 3 ++- src/optionsGuiScheme.tsx | 18 ++++++++++++++++++ src/react/OptionsItems.tsx | 13 +++++++++++-- src/sounds/musicSystem.ts | 4 ++-- 5 files changed, 53 insertions(+), 15 deletions(-) diff --git a/src/basicSounds.ts b/src/basicSounds.ts index 37f8dccd..54af0d35 100644 --- a/src/basicSounds.ts +++ b/src/basicSounds.ts @@ -7,7 +7,12 @@ let audioContext: AudioContext const sounds: Record = {} // Track currently playing sounds and their gain nodes -const activeSounds: Array<{ source: AudioBufferSourceNode; gainNode: GainNode; volumeMultiplier: number }> = [] +const activeSounds: Array<{ + source: AudioBufferSourceNode; + gainNode: GainNode; + volumeMultiplier: number; + isMusic: boolean; +}> = [] window.activeSounds = activeSounds // load as many resources on page load as possible instead on demand as user can disable internet connection after he thinks the page is loaded @@ -43,7 +48,7 @@ export async function loadSound (path: string, contents = path) { } } -export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = options.remoteSoundsLoadTimeout, loop = false) => { +export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = options.remoteSoundsLoadTimeout, loop = false, isMusic = false) => { const soundBuffer = sounds[url] if (!soundBuffer) { const start = Date.now() @@ -51,11 +56,11 @@ export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = option if (cancelled || Date.now() - start > loadTimeout) return } - return playSound(url, soundVolume, loop) + return playSound(url, soundVolume, loop, isMusic) } -export async function playSound (url, soundVolume = 1, loop = false) { - const volume = soundVolume * (options.volume / 100) +export async function playSound (url, soundVolume = 1, loop = false, isMusic = false) { + const volume = soundVolume * (options.volume / 100) * (isMusic ? options.musicVolume / 100 : 1) if (!volume) return @@ -82,7 +87,7 @@ export async function playSound (url, soundVolume = 1, loop = false) { source.start(0) // Add to active sounds - activeSounds.push({ source, gainNode, volumeMultiplier: soundVolume }) + activeSounds.push({ source, gainNode, volumeMultiplier: soundVolume, isMusic }) const callbacks = [] as Array<() => void> source.onended = () => { @@ -110,6 +115,7 @@ export async function playSound (url, soundVolume = 1, loop = false) { console.warn('Failed to stop sound:', err) } }, + gainNode, } } @@ -137,11 +143,11 @@ export function stopSound (url: string) { } } -export function changeVolumeOfCurrentlyPlayingSounds (newVolume: number) { +export function changeVolumeOfCurrentlyPlayingSounds (newVolume: number, newMusicVolume: number) { const normalizedVolume = newVolume / 100 - for (const { gainNode, volumeMultiplier } of activeSounds) { + for (const { gainNode, volumeMultiplier, isMusic } of activeSounds) { try { - gainNode.gain.value = normalizedVolume * volumeMultiplier + gainNode.gain.value = normalizedVolume * volumeMultiplier * (isMusic ? newMusicVolume / 100 : 1) } catch (err) { console.warn('Failed to change sound volume:', err) } @@ -149,5 +155,9 @@ export function changeVolumeOfCurrentlyPlayingSounds (newVolume: number) { } subscribeKey(options, 'volume', () => { - changeVolumeOfCurrentlyPlayingSounds(options.volume) + changeVolumeOfCurrentlyPlayingSounds(options.volume, options.musicVolume) +}) + +subscribeKey(options, 'musicVolume', () => { + changeVolumeOfCurrentlyPlayingSounds(options.volume, options.musicVolume) }) diff --git a/src/defaultOptions.ts b/src/defaultOptions.ts index 85ebae17..48c1cfad 100644 --- a/src/defaultOptions.ts +++ b/src/defaultOptions.ts @@ -16,7 +16,8 @@ export const defaultOptions = { chatOpacityOpened: 100, messagesLimit: 200, volume: 50, - enableMusic: false, + enableMusic: true, + musicVolume: 50, // fov: 70, fov: 75, defaultPerspective: 'first_person' as 'first_person' | 'third_person_back' | 'third_person_front', diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx index a47c06eb..0cb0fe1e 100644 --- a/src/optionsGuiScheme.tsx +++ b/src/optionsGuiScheme.tsx @@ -480,6 +480,24 @@ export const guiOptionsScheme: { ], sound: [ { volume: {} }, + { + custom () { + return { + options.musicVolume = value + }} + item={{ + type: 'slider', + id: 'musicVolume', + text: 'Music Volume', + min: 0, + max: 100, + unit: '%', + }} + /> + }, + }, { custom () { return