From 3b94889bed40e9c687be52c5ca9a87172c6c6a9d Mon Sep 17 00:00:00 2001 From: Vitaly Date: Sat, 20 Sep 2025 01:57:59 +0200 Subject: [PATCH 01/10] 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 02/10] 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 03/10] 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 04/10] 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 05/10] 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 06/10] 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 07/10] 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 08/10] 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