From 0f9d4bc8946f6689aeee2f451bc259cccea5d058 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Tue, 14 Nov 2023 23:10:58 +0300 Subject: [PATCH 0001/1316] test --- prismarine-viewer/viewer/lib/viewer.ts | 70 ++++++++++++++++++- .../viewer/lib/worldDataEmitter.ts | 8 ++- src/index.ts | 2 - 3 files changed, 74 insertions(+), 6 deletions(-) diff --git a/prismarine-viewer/viewer/lib/viewer.ts b/prismarine-viewer/viewer/lib/viewer.ts index 92860fbf..ca5bc144 100644 --- a/prismarine-viewer/viewer/lib/viewer.ts +++ b/prismarine-viewer/viewer/lib/viewer.ts @@ -21,10 +21,12 @@ export class Viewer { isSneaking: boolean version: string cameraObjectOverride?: THREE.Object3D // for xr + /** default sky color */ + skyColour = new THREE.Color('lightblue') - constructor (public renderer: THREE.WebGLRenderer, numWorkers?: number) { + constructor(public renderer: THREE.WebGLRenderer, numWorkers?: number) { this.scene = new THREE.Scene() - this.scene.background = new THREE.Color('lightblue') + this.scene.background = this.skyColour this.ambientLight = new THREE.AmbientLight(0xcc_cc_cc) this.scene.add(this.ambientLight) @@ -50,6 +52,11 @@ export class Viewer { this.world.resetWorld() this.entities.clear() this.primitives.clear() + + this.scene.background = this.skyColour + this.ambientLight.intensity = 1 + this.directionalLight.intensity = 1 + this.directionalLight.position.set(1, 1, 0.5).normalize() } setVersion (userVersion: string) { @@ -81,6 +88,61 @@ export class Viewer { this.primitives.update(p) } + updateTimecycleLighting (timeOfDay, moonPhase, isRaining) { + if (timeOfDay === undefined) { return } + const lightIntensity = this.calculateIntensity(timeOfDay) + const newSkyColor = `#${this.darkenSkyColour(lightIntensity, isRaining).padStart(6, '0')}` + + function timeToRads (time) { + return time * (Math.PI / 12000) + } + + // Update colours + this.scene.background = new THREE.Color(newSkyColor) + const newAmbientIntensity = Math.min(0.43, lightIntensity * 0.75) + (0.04 - (moonPhase / 100)) + const newDirectionalIntensity = Math.min(0.63, lightIntensity) + (0.06 - (moonPhase / 100)) + this.ambientLight.intensity = newAmbientIntensity + this.directionalLight.intensity = newDirectionalIntensity + this.directionalLight.position.set( + Math.cos(timeToRads(timeOfDay)), + Math.sin(timeToRads(timeOfDay)), + 0.2 + ).normalize() + } + + calculateIntensity (currentTicks) { + const transitionStart = 12000 + const transitionEnd = 18000 + const timeInDay = (currentTicks % 24000) + let lightIntensity: number + + if (timeInDay < transitionStart) { + lightIntensity = 1.0 + } else if (timeInDay < transitionEnd) { + lightIntensity = 1 - (timeInDay - transitionStart) / (transitionEnd - transitionStart) + } else { + lightIntensity = (timeInDay - transitionEnd) / (24000 - transitionEnd) + } + + return lightIntensity + } + + /** Darken by factor (0 to black, 0.5 half as bright, 1 unchanged) */ + darkenSkyColour (factor: number, isRaining) { + const skyColour = this.skyColour.getHex() + let r = (skyColour & 0x00_00_FF); + let g = ((skyColour >> 8) & 0x00_FF); + let b = (skyColour >> 16); + if (isRaining) { + r = 111 / 255 + g = 156 / 255 + b = 236 / 255 + } + return (Math.round(r * factor) | + (Math.round(g * factor) << 8) | + (Math.round(b * factor) << 16)).toString(16) + } + setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number, roll = 0) { const cam = this.cameraObjectOverride || this.camera if (pos) { @@ -122,6 +184,10 @@ export class Viewer { this.world.updateViewerPosition(pos) }) + emitter.on('timecycleUpdate', ({ timeOfDay, moonPhase, isRaining }) => { + this.updateTimecycleLighting(timeOfDay, moonPhase, isRaining) + }) + emitter.emit('listening') this.domElement.addEventListener('pointerdown', (evt) => { diff --git a/prismarine-viewer/viewer/lib/worldDataEmitter.ts b/prismarine-viewer/viewer/lib/worldDataEmitter.ts index 9ecb1f43..e303005d 100644 --- a/prismarine-viewer/viewer/lib/worldDataEmitter.ts +++ b/prismarine-viewer/viewer/lib/worldDataEmitter.ts @@ -4,6 +4,7 @@ import { chunkPos } from './simpleUtils' import { generateSpiralMatrix, ViewRect } from 'flying-squid/src/utils' import { Vec3 } from 'vec3' import { EventEmitter } from 'events' +import { BotEvents } from 'mineflayer' export type ChunkPosKey = string type ChunkPos = { x: number, z: number } @@ -54,8 +55,11 @@ export class WorldDataEmitter extends EventEmitter { blockUpdate: (oldBlock: any, newBlock: any) => { const stateId = newBlock.stateId ? newBlock.stateId : ((newBlock.type << 4) | newBlock.metadata) this.emitter.emit('blockUpdate', { pos: oldBlock.position, stateId }) - } - } + }, + time: () => { + this.emitter.emit('timecycleUpdate', { timeOfDay: bot.time.timeOfDay, moonPhase: bot.time.moonPhase, isRaining: bot.isRaining }) + }, + } satisfies Partial this.emitter.on('listening', () => { this.emitter.emit('blockEntities', new Proxy({}, { diff --git a/src/index.ts b/src/index.ts index 948f4500..e1ca2bc4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -560,8 +560,6 @@ async function connect (connectOptions: { worldView.listenToBot(bot) void worldView.init(bot.entity.position) - dayCycle() - // Bot position callback function botPosition () { // this might cause lag, but not sure From badf510fed273fcc52eee595f7654bdbfed974ac Mon Sep 17 00:00:00 2001 From: Vitaly Date: Tue, 14 Nov 2023 23:39:12 +0300 Subject: [PATCH 0002/1316] fix: map provider button now doesn't open a new tab --- src/menus/components/common.js | 8 ++++++-- src/react/MainMenu.tsx | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/menus/components/common.js b/src/menus/components/common.js index 593655d7..5ad74faf 100644 --- a/src/menus/components/common.js +++ b/src/menus/components/common.js @@ -44,8 +44,12 @@ function isProbablyIphone () { /** * @param {string} url */ -function openURL (url) { - window.open(url, '_blank', 'noopener,noreferrer') +function openURL (url, newTab = true) { + if (newTab) { + window.open(url, '_blank', 'noopener,noreferrer') + } else { + window.open(url) + } } export { diff --git a/src/react/MainMenu.tsx b/src/react/MainMenu.tsx index 2c994bcf..81314245 100644 --- a/src/react/MainMenu.tsx +++ b/src/react/MainMenu.tsx @@ -127,7 +127,7 @@ export default ({ connectToServerAction, mapsProvider, singleplayerAction, optio className={styles['maps-provider']} icon='pixelarticons:map' initialTooltip={{ content: 'Explore maps to play from provider!', placement: 'right' }} - onClick={() => openURL(httpsRegex.test(mapsProvider) ? mapsProvider : 'https://' + mapsProvider)} + onClick={() => openURL(httpsRegex.test(mapsProvider) ? mapsProvider : 'https://' + mapsProvider, false)} />} ) From 9240e6c44cf644f74f871665720f5662e8855a20 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Fri, 17 Nov 2023 00:56:56 +0300 Subject: [PATCH 0003/1316] fix: delay chunks load when perf is too bad --- prismarine-viewer/viewer/lib/worldrenderer.ts | 28 ++++++++++++++++--- src/index.ts | 12 ++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/prismarine-viewer/viewer/lib/worldrenderer.ts b/prismarine-viewer/viewer/lib/worldrenderer.ts index 3f022a73..3fc4b487 100644 --- a/prismarine-viewer/viewer/lib/worldrenderer.ts +++ b/prismarine-viewer/viewer/lib/worldrenderer.ts @@ -33,10 +33,15 @@ export class WorldRenderer { downloadedTextureImage = undefined as any workers: any[] = [] viewerPosition?: Vec3 + lastCamUpdate = 0 + droppedFpsPercentage = 0 + initialChunksLoad = true texturesVersion?: string - constructor (public scene: THREE.Scene, numWorkers = 4) { + promisesQueue = [] as Promise[] + + constructor(public scene: THREE.Scene, numWorkers = 4) { // init workers for (let i = 0; i < numWorkers; i++) { // Node environment needs an absolute path, but browser needs the url of the file @@ -59,7 +64,21 @@ export class WorldRenderer { } const chunkCoords = data.key.split(',') - if (!this.loadedChunks[chunkCoords[0] + ',' + chunkCoords[2]] || !data.geometry.positions.length) return + if (!this.loadedChunks[chunkCoords[0] + ',' + chunkCoords[2]] || !data.geometry.positions.length || !this.active) return + + if (!this.initialChunksLoad) { + const newPromise = new Promise(resolve => { + if (this.droppedFpsPercentage > 0.5) { + setTimeout(resolve, 1000 / 50 * this.droppedFpsPercentage) + } else { + setTimeout(resolve) + } + }) + this.promisesQueue.push(newPromise) + for (const promise of this.promisesQueue) { + await promise + } + } const geometry = new THREE.BufferGeometry() geometry.setAttribute('position', new THREE.BufferAttribute(data.geometry.positions, 3)) @@ -156,7 +175,7 @@ export class WorldRenderer { for (const object of Object.values(this.sectionObjects)) { for (const child of object.children) { if (child.name === 'helper') { - child.visible = value; + child.visible = value } } } @@ -224,12 +243,13 @@ export class WorldRenderer { const [currentX, currentZ] = chunkPos(pos) return Object.fromEntries(Object.entries(this.sectionObjects).map(([key, o]) => { const [xRaw, yRaw, zRaw] = key.split(',').map(Number) - const [x, z] = chunkPos({x: xRaw, z: zRaw}) + const [x, z] = chunkPos({ x: xRaw, z: zRaw }) return [`${x - currentX},${z - currentZ}`, o] })) } addColumn (x, z, chunk) { + this.initialChunksLoad = false this.loadedChunks[`${x},${z}`] = true for (const worker of this.workers) { worker.postMessage({ type: 'chunk', x, z, chunk }) diff --git a/src/index.ts b/src/index.ts index 948f4500..90142e7a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -126,6 +126,8 @@ let delta = 0 let lastTime = performance.now() let previousWindowWidth = window.innerWidth let previousWindowHeight = window.innerHeight +let max = 0 +let rendered = 0 const renderFrame = (time: DOMHighResTimeStamp) => { if (window.stopLoop) return window.requestAnimationFrame(renderFrame) @@ -149,10 +151,18 @@ const renderFrame = (time: DOMHighResTimeStamp) => { statsStart() viewer.update() renderer.render(viewer.scene, viewer.camera) + rendered++ postRenderFrameFn() statsEnd() } renderFrame(performance.now()) +setInterval(() => { + if (max > 0) { + viewer.world.droppedFpsPercentage = rendered / max + } + max = Math.max(rendered, max) + rendered = 0 +}, 1000) const resizeHandler = () => { const width = window.innerWidth @@ -564,6 +574,7 @@ async function connect (connectOptions: { // Bot position callback function botPosition () { + viewer.world.lastCamUpdate = Date.now() // this might cause lag, but not sure viewer.setFirstPersonCamera(bot.entity.position, bot.entity.yaw, bot.entity.pitch) void worldView.updatePosition(bot.entity.position) @@ -576,6 +587,7 @@ async function connect (connectOptions: { const maxPitch = 0.5 * Math.PI const minPitch = -0.5 * Math.PI mouseMovePostHandle = ({ x, y }) => { + viewer.world.lastCamUpdate = Date.now() bot.entity.pitch -= y bot.entity.pitch = Math.max(minPitch, Math.min(maxPitch, bot.entity.pitch)) bot.entity.yaw -= x From 755c7aa153675b5a7692820ea4c63c69d0ef28d8 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Wed, 22 Nov 2023 22:06:49 +0300 Subject: [PATCH 0004/1316] always lowercase entity names --- prismarine-viewer/viewer/lib/entities.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/prismarine-viewer/viewer/lib/entities.js b/prismarine-viewer/viewer/lib/entities.js index e46ce437..3d9e994e 100644 --- a/prismarine-viewer/viewer/lib/entities.js +++ b/prismarine-viewer/viewer/lib/entities.js @@ -30,7 +30,8 @@ function getUsernameTexture(username, { fontFamily = 'sans-serif' }) { function getEntityMesh (entity, scene, options) { if (entity.name) { try { - const e = new Entity('1.16.4', entity.name, scene) + // https://github.com/PrismarineJS/prismarine-viewer/pull/410 + const e = new Entity('1.16.4', entity.name.toLowerCase(), scene) if (entity.username !== undefined) { const canvas = getUsernameTexture(entity.username, options) From e8277fb4cefd181767d80cb406837c6297fc833c Mon Sep 17 00:00:00 2001 From: Vitaly Date: Thu, 23 Nov 2023 00:40:54 +0300 Subject: [PATCH 0005/1316] feat: now defaultProxy and defaultHost serve as default values when nothing is entered (behave as fallback values) use new config params with save at the end for previous behavior (but still deprecated) --- config.json | 3 +- src/globalState.ts | 7 ++++- src/menus/components/edit_box.js | 5 +++ src/menus/play_screen.js | 54 ++++++++++++++++++++++++++------ 4 files changed, 58 insertions(+), 11 deletions(-) diff --git a/config.json b/config.json index 123d8f3f..7c0ce254 100644 --- a/config.json +++ b/config.json @@ -1,5 +1,6 @@ { - "defaultHost": "pjs.deptofcraft.com", + "version": 1, + "defaultHost": "", "defaultProxy": "zardoy.site:2344", "defaultVersion": "1.18.2", "mapsProvider": "zardoy.site/maps" diff --git a/src/globalState.ts b/src/globalState.ts index 9815ca69..92bbb5c1 100644 --- a/src/globalState.ts +++ b/src/globalState.ts @@ -120,7 +120,12 @@ export const showContextmenu = (items: ContextMenuItem[], { clientX, clientY }) // --- -type AppConfig = { +export type AppConfig = { + defaultHost?: string + defaultHostSave?: string + defaultProxy?: string + defaultProxySave?: string + defaultVersion?: string mapsProvider?: string } diff --git a/src/menus/components/edit_box.js b/src/menus/components/edit_box.js index 0f42527a..c7210d43 100644 --- a/src/menus/components/edit_box.js +++ b/src/menus/components/edit_box.js @@ -114,6 +114,10 @@ class EditBox extends LitElement { type: Boolean, attribute: 'pmui-required' }, + placeholder: { + type: String, + attribute: 'pmui-placeholder' + }, state: { type: String, attribute: true @@ -144,6 +148,7 @@ class EditBox extends LitElement { autocomplete="off" autocapitalize="off" value="${this.value}" + placeholder=${ifDefined(this.placeholder || undefined)} list=${ifDefined(this.autocompleteValues ? `${this.id}-list` : undefined)} inputmode=${ifDefined(this.inputMode || undefined)} @input=${({ target: { value } }) => { this.value = this.inputMode === 'decimal' ? value.replaceAll(',', '.') : value }} diff --git a/src/menus/play_screen.js b/src/menus/play_screen.js index 7533ddb7..1deb7dc6 100644 --- a/src/menus/play_screen.js +++ b/src/menus/play_screen.js @@ -70,8 +70,10 @@ class PlayScreen extends LitElement { static get properties () { return { server: { type: String }, + serverImplicit: { type: String }, serverport: { type: Number }, proxy: { type: String }, + proxyImplicit: { type: String }, proxyport: { type: Number }, username: { type: String }, password: { type: String }, @@ -84,11 +86,17 @@ class PlayScreen extends LitElement { this.version = '' this.serverport = '' this.proxyport = '' + this.server = '' + this.proxy = '' + this.username = '' + this.password = '' + this.serverImplicit = '' + this.proxyImplicit = '' // todo set them sooner add indicator void window.fetch('config.json').then(async res => res.json()).then(c => c, (error) => { - console.error('Failed to load config.json', error) + console.warn('Failed to load optional config.json', error) return {} - }).then(config => { + }).then(async (/** @type {import('../globalState').AppConfig} */config) => { miscUiState.appConfig = config const params = new URLSearchParams(window.location.search) @@ -100,9 +108,34 @@ class PlayScreen extends LitElement { return qsValue || window.localStorage.getItem(localStorageKey) } - this.server = getParam('server', 'ip') ?? config.defaultHost - this.proxy = getParam('proxy') ?? config.defaultProxy - this.version = getParam('version') || (window.localStorage.getItem('version') ?? config.defaultVersion) + if (config.defaultHost === '' || config.defaultHostSave === '') { + let proxy = config.defaultProxy || config.defaultProxySave || params.get('proxy') + const cleanUrl = url => url.replaceAll(/(https?:\/\/|\/$)/g, '') + if (proxy && cleanUrl(proxy) !== cleanUrl(location.origin + location.pathname)) { + if (!proxy.startsWith('http')) proxy = 'https://' + proxy + const proxyConfig = await fetch(proxy + '/config.json').then(async res => res.json()).then(c => c, (error) => { + console.warn(`Failed to load config.json from proxy ${proxy}`, error) + return {} + }) + if (config.defaultHost === '' && proxyConfig.defaultHost) { + config.defaultHost = proxyConfig.defaultHost + } else { + config.defaultHost = '' + } + if (config.defaultHostSave === '' && proxyConfig.defaultHostSave) { + config.defaultHostSave = proxyConfig.defaultHostSave + } else { + config.defaultHostSave = '' + } + } + this.server = this.serverImplicit + } + + this.serverImplicit = config.defaultHost ?? '' + this.proxyImplicit = config.defaultProxy ?? '' + this.server = getParam('server', 'ip') ?? config.defaultHostSave ?? '' + this.proxy = getParam('proxy') ?? config.defaultProxySave ?? '' + this.version = getParam('version') || (window.localStorage.getItem('version') ?? config.defaultVersion ?? '') this.username = getParam('username') || 'pviewer' + (Math.floor(Math.random() * 1000)) this.password = getParam('password') || '' if (process.env.NODE_ENV === 'development' && params.get('reconnect') && this.server && this.username) { @@ -125,7 +158,8 @@ class PlayScreen extends LitElement { pmui-id="serverip" pmui-value="${this.server}" pmui-type="url" - pmui-required="true" + pmui-required="${this.serverImplicit === ''}}" + pmui-placeholder="${this.serverImplicit}" .autocompleteValues=${JSON.parse(localStorage.getItem('serverHistory') || '[]')} @input=${e => { this.server = e.target.value }} > @@ -135,6 +169,7 @@ class PlayScreen extends LitElement { pmui-id="port" pmui-value="${this.serverport}" pmui-type="number" + pmui-placeholder="25565" @input=${e => { this.serverport = e.target.value }} > @@ -144,7 +179,8 @@ class PlayScreen extends LitElement { pmui-label="Proxy IP" pmui-id="proxy" pmui-value="${this.proxy}" - pmui-required=${/* TODO derive from config? */false} + pmui-required="${this.proxyImplicit === ''}}" + pmui-placeholder="${this.proxyImplicit}" pmui-type="url" @input=${e => { this.proxy = e.target.value }} > @@ -190,8 +226,8 @@ class PlayScreen extends LitElement { } onConnectPress () { - const server = `${this.server}${this.serverport && `:${this.serverport}`}` - const proxy = this.proxy && `${this.proxy}${this.proxyport && `:${this.proxyport}`}` + const server = this.server ? `${this.server}${this.serverport && `:${this.serverport}`}` : this.serverImplicit + const proxy = this.proxy ? `${this.proxy}${this.proxyport && `:${this.proxyport}`}` : this.proxyImplicit window.localStorage.setItem('username', this.username) window.localStorage.setItem('password', this.password) From 98231242a1d2dc74b517aa26ec449a5ac434d614 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Thu, 23 Nov 2023 00:48:53 +0300 Subject: [PATCH 0006/1316] don't use forwardRef anti-pattern fix: make first-time tooltip buttons less-annoying --- src/react/Button.tsx | 9 +++++---- src/react/ButtonWithTooltip.tsx | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/react/Button.tsx b/src/react/Button.tsx index fb9e3f04..4d346b14 100644 --- a/src/react/Button.tsx +++ b/src/react/Button.tsx @@ -1,5 +1,5 @@ -import { forwardRef } from 'react' import classNames from 'classnames' +import { FC, Ref } from 'react' import { loadSound, playSound } from '../basicSounds' import buttonCss from './button.module.css' @@ -10,11 +10,12 @@ interface Props extends React.ComponentProps<'button'> { icon?: string children?: React.ReactNode inScreen?: boolean + rootRef?: Ref } void loadSound('button_click.mp3') -export default forwardRef(({ label, icon, children, inScreen, ...args }, ref) => { +export default (({ label, icon, children, inScreen, rootRef, ...args }) => { const onClick = (e) => { void playSound('button_click.mp3') args.onClick?.(e) @@ -28,9 +29,9 @@ export default forwardRef(({ label, icon, children, in args.style.width = 20 } - return -}) +}) satisfies FC diff --git a/src/react/ButtonWithTooltip.tsx b/src/react/ButtonWithTooltip.tsx index 390b074c..842f5a2e 100644 --- a/src/react/ButtonWithTooltip.tsx +++ b/src/react/ButtonWithTooltip.tsx @@ -51,20 +51,21 @@ export default ({ initialTooltip, ...args }: Props) => { }) return <> - +
From 20b6344deb1ef66716969ed546e3488541848ae9 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Fri, 24 Nov 2023 13:52:00 +0300 Subject: [PATCH 0010/1316] refactor: open github cleanup --- src/menus/pause_screen.js | 10 ++++------ src/react/MainMenuRenderApp.tsx | 4 ++-- src/utils.ts | 4 ++++ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/menus/pause_screen.js b/src/menus/pause_screen.js index ca63309f..f8042ea5 100644 --- a/src/menus/pause_screen.js +++ b/src/menus/pause_screen.js @@ -4,7 +4,7 @@ const { subscribe } = require('valtio') const { subscribeKey } = require('valtio/utils') const { hideCurrentModal, showModal, miscUiState, notification, openOptionsMenu } = require('../globalState') const { fsState } = require('../loadSave') -const { disconnect } = require('../utils') +const { disconnect, openGithub } = require('../utils') const { closeWan, openToWanAndCopyJoinLink, getJoinLink } = require('../localServerMultiplayer') const { uniqueFileNameFromWorldName, copyFilesAsyncWithProgress } = require('../browserfs') const { showOptionsModal } = require('../react/SelectOption') @@ -89,9 +89,7 @@ class PauseScreen extends LitElement {
- openURL( - // @ts-expect-error - process.env.GITHUB_URL)}> + openGithub()}> openURL('https://discord.gg/4Ucm684Fq3')}>
openOptionsMenu('main')}> @@ -104,8 +102,8 @@ class PauseScreen extends LitElement {
` : ''} { - disconnect() - }}> + disconnect() + }}> ` } diff --git a/src/react/MainMenuRenderApp.tsx b/src/react/MainMenuRenderApp.tsx index 9e06bf66..c5655c00 100644 --- a/src/react/MainMenuRenderApp.tsx +++ b/src/react/MainMenuRenderApp.tsx @@ -4,7 +4,7 @@ import { useSnapshot } from 'valtio' import { useEffect } from 'react' import { activeModalStack, miscUiState, openOptionsMenu, showModal } from '../globalState' import { openURL } from '../menus/components/common' -import { openFilePicker, setLoadingScreenStatus } from '../utils' +import { openFilePicker, openGithub, setLoadingScreenStatus } from '../utils' import { copyFilesAsync, mkdirRecursive, openWorldDirectory, removeFileRecursiveAsync } from '../browserfs' import MainMenu from './MainMenu' @@ -46,7 +46,7 @@ export default () => { } showModal({ reactType: 'singleplayer' }) }} - githubAction={() => openURL(process.env.GITHUB_URL!)} + githubAction={() => openGithub()} optionsAction={() => openOptionsMenu('main')} discordAction={() => openURL('https://discord.gg/4Ucm684Fq3')} openFileAction={e => { diff --git a/src/utils.ts b/src/utils.ts index 882ecf0b..9d245132 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -213,6 +213,10 @@ export const openFilePicker = (specificCase?: 'resourcepack') => { picker.click() } +export const openGithub = () => { + window.open(process.env.GITHUB_URL, '_blank') +} + export const resolveTimeout = async (promise, timeout = 10_000) => { return new Promise((resolve, reject) => { promise.then(resolve, reject) From 2b6acc54bf40eb360cac5c0b7132a02f71e5df4c Mon Sep 17 00:00:00 2001 From: Vitaly Date: Mon, 4 Dec 2023 04:02:37 +0300 Subject: [PATCH 0011/1316] feat: now you can inspect .mca region files in browser console by drag-n-dropping them into the browser window! It also can detect version of region file. --- src/dragndrop.ts | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/dragndrop.ts b/src/dragndrop.ts index 0a18afe8..83aeb99f 100644 --- a/src/dragndrop.ts +++ b/src/dragndrop.ts @@ -1,9 +1,13 @@ import { promisify } from 'util' +import fs from 'fs' import * as nbt from 'prismarine-nbt' +import RegionFile from 'prismarine-provider-anvil/src/region' +import { versions } from 'minecraft-data' import { openWorldDirectory, openWorldZip } from './browserfs' import { isGameActive, showNotification } from './globalState' const parseNbt = promisify(nbt.parse) +const simplifyNbt = nbt.simplify window.nbt = nbt // todo display drop zone @@ -48,6 +52,48 @@ async function handleDroppedFile (file: File) { alert('Rar files are not supported yet!') return } + if (file.name.endsWith('.mca')) { + const tempPath = '/data/temp.mca' + try { + await fs.promises.writeFile(tempPath, Buffer.from(await file.arrayBuffer())) + const region = new RegionFile(tempPath) + await region.initialize() + const chunks: Record = {} + console.log('Reading chunks...') + console.log(chunks) + let versionDetected = false + for (const [i, _] of Array.from({ length: 32 }).entries()) { + for (const [k, _] of Array.from({ length: 32 }).entries()) { + const nbt = await region.read(i, k) + chunks[`${i},${k}`] = nbt + if (nbt && !versionDetected) { + const simplified = simplifyNbt(nbt) + const version = versions.pc.find(x => x['dataVersion'] === simplified.DataVersion)?.minecraftVersion + console.log('Detected version', version ?? 'unknown') + versionDetected = true + } + } + } + Object.defineProperty(chunks, 'simplified', { + get () { + const mapped = {} + for (const [i, _] of Array.from({ length: 32 }).entries()) { + for (const [k, _] of Array.from({ length: 32 }).entries()) { + const key = `${i},${k}` + const chunk = chunks[key] + if (!chunk) continue + mapped[key] = simplifyNbt(chunk) + } + } + return mapped + }, + }) + console.log('Done!', chunks) + } finally { + await fs.promises.unlink(tempPath) + } + return + } const buffer = await file.arrayBuffer() const parsed = await parseNbt(Buffer.from(buffer)).catch((err) => { From 1ba82a26abad7fb92a9a84d6f37f8890654318ed Mon Sep 17 00:00:00 2001 From: Vitaly Date: Sun, 17 Dec 2023 22:48:16 +0300 Subject: [PATCH 0012/1316] add canonical to use mcraft.fun as main domain for google search. it would reset all scores & index to 0 sadly --- README.MD | 4 ++-- index.html | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.MD b/README.MD index 946ce5f6..d198c8bb 100644 --- a/README.MD +++ b/README.MD @@ -4,6 +4,8 @@ A true Minecraft client running in your browser! A port of the original game to This project is a work in progress, but I consider it to be usable. If you encounter any bugs or usability issues, please report them! +You can try this out at [mcraft.fun](https://mcraft.fun/), [mcon.vercel.app](https://mcon.vercel.app/) or the GitHub pages deploy. + ### Big Features - Connect to any offline server* (it's possible because of proxy servers, see below) @@ -15,8 +17,6 @@ This project is a work in progress, but I consider it to be usable. If you encou - Resource pack support - even even more! -There are a lot - ### World Loading Zip files and folders are supported. Just drag and drop them into the browser window. You can open folders in readonly and read-write mode. New chunks may be generated incorrectly for now. diff --git a/index.html b/index.html index d3a2bbae..8ab8ad3b 100644 --- a/index.html +++ b/index.html @@ -24,16 +24,16 @@ + - + - - + From 947b59632dad9b7edc87e8644e1937fc9c4cc4d3 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Sun, 17 Dec 2023 22:55:57 +0300 Subject: [PATCH 0013/1316] up tsx --- pnpm-lock.yaml | 18 +++++++++--------- prismarine-viewer/package.json | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10f7c83d..a78faab4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -340,8 +340,8 @@ importers: specifier: ^1.3.0 version: 1.4.0 tsx: - specifier: ^3.13.0 - version: 3.14.0 + specifier: ^4.6.2 + version: 4.6.2 vec3: specifier: ^0.1.7 version: 0.1.8 @@ -13444,13 +13444,13 @@ packages: /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - /tsx@3.14.0: - resolution: {integrity: sha512-xHtFaKtHxM9LOklMmJdI3BEnQq/D5F73Of2E1GDrITi9sgoVkvIsrQUTY1G8FlmGtA+awCI4EBlTRRYxkL2sRg==} + /tsx@4.6.2: + resolution: {integrity: sha512-QPpBdJo+ZDtqZgAnq86iY/PD2KYCUPSUGIunHdGwyII99GKH+f3z3FZ8XNFLSGQIA4I365ui8wnQpl8OKLqcsg==} + engines: {node: '>=18.0.0'} hasBin: true dependencies: esbuild: 0.18.20 get-tsconfig: 4.7.2 - source-map-support: 0.5.21 optionalDependencies: fsevents: 2.3.3 dev: false @@ -14646,8 +14646,8 @@ packages: - react dev: true - github.com/zardoy/minecraft-protocol/8e61798aeae786db3ddd4bbd9e19b6e30bb2520c: - resolution: {tarball: https://codeload.github.com/zardoy/minecraft-protocol/tar.gz/8e61798aeae786db3ddd4bbd9e19b6e30bb2520c} + github.com/zardoy/minecraft-protocol/436e0f2945d82408cfd1eb4262535c205bcba8d0: + resolution: {tarball: https://codeload.github.com/zardoy/minecraft-protocol/tar.gz/436e0f2945d82408cfd1eb4262535c205bcba8d0} name: minecraft-protocol version: 1.44.0 engines: {node: '>=14'} @@ -14681,7 +14681,7 @@ packages: engines: {node: '>=14'} dependencies: minecraft-data: 3.48.0 - minecraft-protocol: github.com/zardoy/minecraft-protocol/8e61798aeae786db3ddd4bbd9e19b6e30bb2520c + minecraft-protocol: github.com/zardoy/minecraft-protocol/436e0f2945d82408cfd1eb4262535c205bcba8d0 prismarine-biome: 1.3.0(minecraft-data@3.48.0)(prismarine-registry@1.7.0) prismarine-block: github.com/zardoy/prismarine-block/753cf1fe507f7647063c69d5c124d40f85b29cc2 prismarine-chat: 1.9.1 @@ -14767,7 +14767,7 @@ packages: flatmap: 0.0.3 long: 5.2.3 minecraft-data: 3.48.0 - minecraft-protocol: github.com/zardoy/minecraft-protocol/8e61798aeae786db3ddd4bbd9e19b6e30bb2520c + minecraft-protocol: github.com/zardoy/minecraft-protocol/436e0f2945d82408cfd1eb4262535c205bcba8d0 mkdirp: 2.1.6 moment: 2.29.4 needle: 2.9.1 diff --git a/prismarine-viewer/package.json b/prismarine-viewer/package.json index 4e49c6fe..1fe2d6d1 100644 --- a/prismarine-viewer/package.json +++ b/prismarine-viewer/package.json @@ -38,7 +38,7 @@ "socket.io-client": "^4.0.0", "three-stdlib": "^2.26.11", "three.meshline": "^1.3.0", - "tsx": "^3.13.0", + "tsx": "^4.6.2", "vec3": "^0.1.7" } } From 41ea0517fd0774577ee6de4690782a026b438b7d Mon Sep 17 00:00:00 2001 From: Vitaly Date: Mon, 1 Jan 2024 18:35:54 +0530 Subject: [PATCH 0014/1316] fix: don't crash when R is pressed after not-first successful connect --- index.html | 1 - src/index.ts | 28 +++++++++------------------ src/react/AppStatusProvider.tsx | 34 ++++++++++++++++++++++++--------- src/standaloneUtils.ts | 3 +++ src/topRightStats.ts | 2 +- src/utils.ts | 4 ---- 6 files changed, 38 insertions(+), 34 deletions(-) create mode 100644 src/standaloneUtils.ts diff --git a/index.html b/index.html index d3a2bbae..b79c0af1 100644 --- a/index.html +++ b/index.html @@ -43,7 +43,6 @@ - diff --git a/src/index.ts b/src/index.ts index 90142e7a..471b7137 100644 --- a/src/index.ts +++ b/src/index.ts @@ -55,11 +55,11 @@ import { } from './globalState' import { - pointerLock, isCypress, + pointerLock, toMajorVersion, - setLoadingScreenStatus, - setRenderDistance + setLoadingScreenStatus } from './utils' +import { isCypress } from './standaloneUtils' import { removePanorama @@ -77,7 +77,7 @@ import CustomChannelClient from './customClient' import debug from 'debug' import { loadScript } from 'prismarine-viewer/viewer/lib/utils' import { registerServiceWorker } from './serviceWorker' -import { appStatusState } from './react/AppStatusProvider' +import { appStatusState, lastConnectOptions } from './react/AppStatusProvider' import { fsState } from './loadSave' import { watchFov } from './rendererUtils' @@ -219,14 +219,12 @@ const loadSingleplayer = (serverOverrides = {}, flattenedServerOverrides = {}) = void connect({ singleplayer: true, username: options.localUsername, password: '', serverOverrides, serverOverridesFlat: flattenedServerOverrides }) } function listenGlobalEvents () { - const menu = document.getElementById('play-screen') - menu.addEventListener('connect', e => { - const options = e.detail + window.addEventListener('connect', e => { + const options = (e as CustomEvent).detail void connect(options) }) window.addEventListener('singleplayer', (e) => { - //@ts-expect-error - loadSingleplayer(e.detail) + loadSingleplayer((e as CustomEvent).detail) }) } @@ -259,6 +257,8 @@ const cleanConnectIp = (host: string | undefined, defaultPort: string | undefine async function connect (connectOptions: { server?: string; singleplayer?: any; username: string; password?: any; proxy?: any; botVersion?: any; serverOverrides?; serverOverridesFlat?; peerId?: string }) { + if (miscUiState.gameLoaded) return + lastConnectOptions.value = connectOptions document.getElementById('play-screen').style = 'display: none;' removePanorama() @@ -308,16 +308,6 @@ async function connect (connectOptions: { errorAbortController.abort() console.log('Encountered error!', err) - // #region rejoin key - const controller = new AbortController() - window.addEventListener('keyup', (e) => { - if (e.code !== 'KeyR') return - controller.abort() - void connect(connectOptions) - appStatusState.isError = false - }, { signal: controller.signal }) - // #endregion - setLoadingScreenStatus(`Error encountered. ${err}`, true) destroyAll() if (isCypress()) throw err diff --git a/src/react/AppStatusProvider.tsx b/src/react/AppStatusProvider.tsx index ed2b0337..5a233543 100644 --- a/src/react/AppStatusProvider.tsx +++ b/src/react/AppStatusProvider.tsx @@ -1,4 +1,5 @@ import { proxy, useSnapshot } from 'valtio' +import { useEffect } from 'react' import { activeModalStacks, hideModal, insertActiveModalStack, miscUiState } from '../globalState' import { resetLocalStorageWorld } from '../browserfs' import { fsState } from '../loadSave' @@ -19,6 +20,10 @@ const resetState = () => { Object.assign(appStatusState, initialState) } +export const lastConnectOptions = { + value: null as any | null +} + export default () => { const { isError, lastStatus, maybeRecoverable, status, hideDots } = useSnapshot(appStatusState) @@ -37,10 +42,21 @@ export default () => { } }, [isOpen]) + useEffect(() => { + window.addEventListener('keyup', (e) => { + if (!isOpen) return + if (e.code !== 'KeyR' || !lastConnectOptions.value) return + window.dispatchEvent(new window.CustomEvent('connect', { + detail: lastConnectOptions.value + })) + appStatusState.isError = false + }, {}) + }, []) + return { @@ -55,14 +71,14 @@ export default () => { hideModal(undefined, undefined, { force: true }) } } : undefined} - // actionsSlot={ - // + + + + }, +} + +export default meta +type Story = StoryObj + +export const Primary: Story = { + args: { + messages: [{ + parts: [ + { + 'bold': false, + 'italic': false, + 'underlined': false, + 'strikethrough': false, + 'obfuscated': false, + //@ts-expect-error + 'json': { + 'insertion': 'pviewer672', + 'clickEvent': { + 'action': 'suggest_command', + 'value': '/tell pviewer672 ' + }, + 'hoverEvent': { + 'action': 'show_entity', + 'contents': { + 'type': 'minecraft:player', + 'id': 'ecd0eeb1-625e-3fea-b16e-cb449dcfa434', + 'name': { + 'text': 'pviewer672' + } + } + }, + 'text': 'pviewer672' + }, + 'text': 'pviewer672', + 'clickEvent': { + 'action': 'suggest_command', + 'value': '/tell pviewer672 ' + }, + 'hoverEvent': { + 'action': 'show_entity', + 'contents': { + 'type': 'minecraft:player', + 'id': 'ecd0eeb1-625e-3fea-b16e-cb449dcfa434', + 'name': { + 'text': 'pviewer672' + } + } + } + }, + { + 'text': ' joined the game', + 'color': 'yellow', + 'bold': false, + 'italic': false, + 'underlined': false, + 'strikethrough': false, + 'obfuscated': false + } + ], + id: 0, + }], + opened: false, + async fetchCompletionItems (triggerType, value) { + console.log('fetchCompletionItems') + await new Promise(resolve => { + setTimeout(resolve, 700) + }) + let items = ['test', ...Array.from({ length: 50 }).map((_, i) => `minecraft:hello${i}`)] + if (value === '/') items = items.map(item => `/${item}`) + return items + }, + }, +} diff --git a/src/react/ChatContainer.css b/src/react/ChatContainer.css new file mode 100644 index 00000000..9e54ef50 --- /dev/null +++ b/src/react/ChatContainer.css @@ -0,0 +1,168 @@ +div.chat-wrapper { /* increase specificity */ + position: fixed; + z-index: 10; + padding-left: calc(env(safe-area-inset-left) / 2); + padding-right: calc(env(safe-area-inset-right, 4px) / 2); +} + +.chat-messages-wrapper { + bottom: 40px; + padding: 4px; + padding-left: 0; + max-height: var(--chatHeight); + width: var(--chatWidth); + transform-origin: bottom left; + transform: scale(var(--chatScale)); + pointer-events: none; +} + +.chat-input-wrapper { + bottom: 1px; + width: calc(100% - 3px); + position: fixed; + left: 1px; + box-sizing: border-box; + background-color: rgba(0, 0, 0, 0); +} +.chat-input { + box-sizing: border-box; + width: 100%; +} +.chat-completions { + position: absolute; + /* position this bottom on top of parent */ + top: 0; + left: 0; + transform: translateY(-100%); + /* width: 150px; */ + display: flex; + padding: 0 2px; /* input padding */ + width: 100%; +} +.input-mobile .chat-completions { + transform: none; + top: 15px; /* input height */ +} +.chat-completions-pad-text { + pointer-events: none; + white-space: pre; + opacity: 0; + overflow: hidden; +} +.chat-completions-items { + background-color: rgba(0, 0, 0, 0.5); + display: flex; + flex-direction: column; + /* justify-content: flex-end; */ + /* probably would be better to replace with margin, not sure */ + padding: 2px; + max-height: 100px; + overflow: auto; + /* hide ugly scrollbars in firefox */ + scrollbar-width: none; +} +/* unsupported by firefox */ +::-webkit-scrollbar { + width: 5px; + background-color: rgb(24, 24, 24); +} +::-webkit-scrollbar-thumb { + background-color: rgb(50, 50, 50); +} +.chat-completions-items > div { + cursor: pointer; +} +.chat-completions-items > div:hover { + text-shadow: 0px 0px 6px white; +} +.input-mobile .chat-completions-items { + justify-content: flex-start; +} + +.input-mobile { + top: 1px; +} +.input-mobile #chatinput { + height: 20px; +} + +.display-mobile { + top: 40px; +} + +.chat, .chat-input { + color: white; + font-size: 10px; + margin: 0px; + line-height: 100%; + text-shadow: 1px 1px 0px #3f3f3f; + font-family: mojangles, minecraft, monospace; + max-height: var(--chatHeight); +} +.chat { + pointer-events: none; + overflow: hidden; + width: 100%; + scrollbar-width: thin; +} +.chat.opened { + pointer-events: auto; + overflow-y: auto; +} + +input[type=text], #chatinput { + background-color: rgba(0, 0, 0, 0.5); + border: 1px solid rgba(0, 0, 0, 0); + outline: none; + pointer-events: auto; + /* styles reset */ + padding-top: 1px; + padding-bottom: 1px; + padding-left: 2px; + padding-right: 2px; + height: 15px; +} + +.chat-mobile-hidden { + width: 8px; + height: 0; + position: absolute; + display: block !important; + opacity: 0; + pointer-events: none; +} +.chat-mobile-hidden:nth-last-child(1) { + height: 8px; +} + +#chatinput:focus { + border-color: white; +} + +.chat-message { + padding-left: 4px; + background-color: rgba(0, 0, 0, 0.5); + list-style: none; + word-break: break-all; +} + +.chat-message-fadeout { + opacity: 1; + transition: all 3s; +} + +.chat-message-fade { + opacity: 0; +} + +.chat-message-faded { + transition: none !important; +} + +.chat.opened .chat-message { + opacity: 1 !important; + transition: none !important; +} + +.chat-message-part { +} diff --git a/src/react/ChatContainer.tsx b/src/react/ChatContainer.tsx new file mode 100644 index 00000000..124a5e10 --- /dev/null +++ b/src/react/ChatContainer.tsx @@ -0,0 +1,278 @@ +import { useEffect, useMemo, useRef, useState } from 'react' +import { isCypress } from '../standaloneUtils' +import { MessageFormatPart } from '../botUtils' +import { MessagePart } from './MessageFormatted' +import './ChatContainer.css' + +type Message = { + parts: MessageFormatPart[], + id: number + fading?: boolean + faded?: boolean +} + +const MessageLine = ({ message }) => { + const classes = { + 'chat-message-fadeout': message.fading, + 'chat-message-fade': message.fading, + 'chat-message-faded': message.faded, + 'chat-message': true + } + + return
  • val).map(([name]) => name).join(' ')}> + {message.parts.map((msg, i) => )} +
  • +} + +type Props = { + messages: Message[] + touch?: boolean + opacity?: number + opened?: boolean + onClose?: () => void + interceptMessage?: (message: string) => boolean + fetchCompletionItems?: (triggerKind: 'implicit' | 'explicit', completeValue: string, fullValue: string, abortController?: AbortController) => Promise + // width?: number +} + +export const initialChatOpenValue = { + value: '' +} + +export const fadeMessage = (message: Message, initialTimeout: boolean, requestUpdate: () => void) => { + setTimeout(() => { + message.fading = true + requestUpdate() + setTimeout(() => { + message.faded = true + requestUpdate() + }, 3000) + }, initialTimeout ? 5000 : 0) +} + +const ChatComponent = ({ messages, touch, opacity, fetchCompletionItems, opened, interceptMessage, onClose }: Props) => { + const [sendHistory, _setSendHistory] = useState(JSON.parse(window.sessionStorage.chatHistory || '[]')) + + const [completePadText, setCompletePadText] = useState('') + const completeRequestValue = useRef('') + const [completionItemsSource, setCompletionItemsSource] = useState([] as string[]) + const [completionItems, setCompletionItems] = useState([] as string[]) + + const chatInput = useRef(null!) + const chatMessages = useRef(null!) + const openedChatWasAtBottom = useRef(false) + const chatHistoryPos = useRef(0) + const inputCurrentlyEnteredValue = useRef('') + + const setSendHistory = (newHistory: string[]) => { + _setSendHistory(newHistory) + window.sessionStorage.chatHistory = JSON.stringify(newHistory) + } + + 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' })) + chatInput.current.focus() + } + + const updateInputValue = (newValue: string) => { + chatInput.current.value = newValue + chatInput.current.dispatchEvent(new Event('input')) + setTimeout(() => { + chatInput.current.setSelectionRange(newValue.length, newValue.length) + }, 0) + } + + useEffect(() => { + // todo focus input on any keypress except tab + + chatInput.current.addEventListener('keypress', e => { + if (e.code === 'Enter') { + const message = chatInput.current.value + if (message) { + setSendHistory([...sendHistory, message]) + const handled = interceptMessage?.(message) + if (!handled) { + bot.chat(message) + } + } + onClose?.() + } + }) + }, []) + + const resetCompletionItems = () => { + setCompletionItemsSource([]) + setCompletionItems([]) + } + + useEffect(() => { + if (opened) { + chatHistoryPos.current = messages.length + updateInputValue(initialChatOpenValue.value) + initialChatOpenValue.value = '' + chatInput.current.focus() + resetCompletionItems() + } + if (!opened) { + chatMessages.current.scrollTop = chatMessages.current.scrollHeight + } + }, [opened]) + + useEffect(() => { + if (!opened || (opened && openedChatWasAtBottom.current)) { + openedChatWasAtBottom.current = false + // stay at bottom on messages changes + chatMessages.current.scrollTop = chatMessages.current.scrollHeight + } + }, [messages]) + + useMemo(() => { + if ((opened && chatMessages.current)) { + const wasAtBottom = chatMessages.current.scrollTop === chatMessages.current.scrollHeight - chatMessages.current.clientHeight + openedChatWasAtBottom.current = wasAtBottom + // console.log(wasAtBottom, chatMessages.current.scrollTop, chatMessages.current.scrollHeight - chatMessages.current.clientHeight) + } + }, [messages]) + + const auxInputFocus = (fireKey: string) => { + chatInput.current.focus() + document.dispatchEvent(new KeyboardEvent('keydown', { code: fireKey })) + } + + const getDefaultCompleteValue = () => { + 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 + const completeValue = lastLength ? value.slice(0, -lastLength) : value + if (valueParts.length === 1 && value.startsWith('/')) return '/' + return completeValue + } + + const fetchCompletions = async (implicit: boolean, inputValue = chatInput.current.value) => { + const completeValue = getCompleteValue(inputValue) + completeRequestValue.current = completeValue + resetCompletionItems() + const newItems = await fetchCompletionItems?.(implicit ? 'implicit' : 'explicit', completeValue, inputValue) ?? [] + if (completeValue !== completeRequestValue.current) return + setCompletionItemsSource(newItems) + updateFilteredCompleteItems(newItems) + } + + const updateFilteredCompleteItems = (sourceItems: string[]) => { + const newCompleteItems = sourceItems.filter(i => { + // this regex is imporatnt is it controls the word matching + const compareableParts = i.split(/[_:]/) + const lastWord = chatInput.current.value.slice(0, chatInput.current.selectionEnd ?? chatInput.current.value.length).split(' ').at(-1)! + return compareableParts.some(compareablePart => compareablePart.startsWith(lastWord)) + }) + setCompletionItems(newCompleteItems) + } + + return ( + <> + + + + + ) +} + +export default ChatComponent diff --git a/src/react/ChatProvider.tsx b/src/react/ChatProvider.tsx new file mode 100644 index 00000000..26189330 --- /dev/null +++ b/src/react/ChatProvider.tsx @@ -0,0 +1,64 @@ +import { useEffect, useState } from 'react' +import { Message } from '../chat' +import { formatMessage } from '../botUtils' +import { getBuiltinCommandsList, tryHandleBuiltinCommand } from '../builtinCommands' +import { hideCurrentModal } from '../globalState' +import { options } from '../optionsStorage' +import ChatComponent, { fadeMessage } from './ChatContainer' +import { useIsModalActive } from './utils' + +export default () => { + const [messages, setMessages] = useState([] as Message[]) + const isChatActive = useIsModalActive('chat') + const messagesLimit = 200 + + useEffect(() => { + bot.addListener('message', (jsonMsg, position) => { + const parts = formatMessage(jsonMsg) + + const lastId = messages.at(-1)?.id ?? 0 + const newMessage: Message = { + parts, + id: lastId + 1, + faded: false, + } + setMessages([...messages, newMessage].slice(-messagesLimit)) + fadeMessage(newMessage, true, () => { + setMessages([...messages]) + }) + + // todo update scrollbottom + }) + }, []) + + return { + const builtinHandled = tryHandleBuiltinCommand(message) + return !!builtinHandled + }} + onClose={() => { + hideCurrentModal() + }} + fetchCompletionItems={async (triggerKind, completeValue) => { + if ((triggerKind === 'explicit' || options.autoRequestCompletions)) { + let items = await bot.tabComplete(completeValue, true, true) + if (typeof items[0] === 'object') { + // @ts-expect-error + if (items[0].match) items = items.map(i => i.match) + } + if (completeValue === '/') { + if (!items[0].startsWith('/')) { + // normalize + items = items.map(item => `/${item}`) + } + if (localServer) { + items = [...items, ...getBuiltinCommandsList()] + } + } + return items + } + }} + /> +} diff --git a/src/react/MessageFormatted.tsx b/src/react/MessageFormatted.tsx index b5f7fe04..04a112cc 100644 --- a/src/react/MessageFormatted.tsx +++ b/src/react/MessageFormatted.tsx @@ -1,6 +1,7 @@ +import { ComponentProps } from 'react' import { MessageFormatPart } from '../botUtils' -const MessagePart = ({ part }: { part: MessageFormatPart }) => { +export const MessagePart = ({ part, ...props }: { part: MessageFormatPart } & ComponentProps<'span'>) => { const { color, italic, bold, underlined, strikethrough, text } = part const applyStyles = [ @@ -12,7 +13,7 @@ const MessagePart = ({ part }: { part: MessageFormatPart }) => { strikethrough && messageFormatStylesMap.strikethrough ].filter(Boolean) - return {text} + return {text} } export default ({ parts }: { parts: readonly MessageFormatPart[] }) => { diff --git a/src/reactUi.jsx b/src/reactUi.jsx index d67a1493..77139bb4 100644 --- a/src/reactUi.jsx +++ b/src/reactUi.jsx @@ -17,6 +17,7 @@ import CreateWorldProvider from './react/CreateWorldProvider' import AppStatusProvider from './react/AppStatusProvider' import SelectOption from './react/SelectOption' import EnterFullscreenButton from './react/EnterFullscreenButton' +import ChatProvider from './react/ChatProvider' // todo useInterfaceState.setState({ @@ -125,6 +126,7 @@ const InGameUi = () => { {/* apply scaling */} + diff --git a/vitest.config.ts b/vitest.config.ts index 7636f5e3..fc9d1e49 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,7 +4,8 @@ export default defineConfig({ root: 'prismarine-viewer/viewer', test: { include: [ - '**/*.test.ts' + '../../src/botUtils.test.ts', + 'sign-renderer/tests.test.ts' ], }, }) From 3893c2d0b1f2fe531d756d5fad07a6e99ca67ca8 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Wed, 3 Jan 2024 19:49:29 +0530 Subject: [PATCH 0017/1316] update imports --- src/controls.ts | 6 ++++-- src/serviceWorker.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/controls.ts b/src/controls.ts index 7e3565f4..c0ac6f23 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -10,6 +10,7 @@ import { isGameActive, showModal, gameAdditionalState, activeModalStack, hideCur import { goFullscreen, pointerLock, reloadChunks } from './utils' import { options } from './optionsStorage' import { openPlayerInventory } from './playerWindows' +import { initialChatOpenValue } from './react/ChatContainer' // doesnt seem to work for now const customKeymaps = proxy(JSON.parse(localStorage.keymap || '{}')) @@ -218,10 +219,11 @@ contro.on('trigger', ({ command }) => { }) break case 'general.chat': - document.getElementById('hud').shadowRoot.getElementById('chat').enableChat() + showModal({ reactType: 'chat' }) break case 'general.command': - document.getElementById('hud').shadowRoot.getElementById('chat').enableChat('/') + initialChatOpenValue.value = '/' + showModal({ reactType: 'chat' }) break case 'general.selectItem': void selectItem() diff --git a/src/serviceWorker.ts b/src/serviceWorker.ts index 2fe00bee..cbb7a42a 100644 --- a/src/serviceWorker.ts +++ b/src/serviceWorker.ts @@ -1,4 +1,4 @@ -import { isCypress } from './utils' +import { isCypress } from './standaloneUtils' export const registerServiceWorker = async () => { if (!('serviceWorker' in navigator)) return From 05c14ecab204eabb16736b4a36621720cafedc47 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Wed, 3 Jan 2024 20:11:06 +0530 Subject: [PATCH 0018/1316] fix imports once again --- src/index.ts | 1 - src/react/ChatContainer.tsx | 2 +- src/react/ChatProvider.tsx | 3 +-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index 471b7137..0a1c2285 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,6 @@ import './importsWorkaround' import './styles.css' import './globals' import 'iconify-icon' -import './chat' import './getCollisionShapes' import { onGameLoad } from './playerWindows' diff --git a/src/react/ChatContainer.tsx b/src/react/ChatContainer.tsx index 124a5e10..33d72a60 100644 --- a/src/react/ChatContainer.tsx +++ b/src/react/ChatContainer.tsx @@ -4,7 +4,7 @@ import { MessageFormatPart } from '../botUtils' import { MessagePart } from './MessageFormatted' import './ChatContainer.css' -type Message = { +export type Message = { parts: MessageFormatPart[], id: number fading?: boolean diff --git a/src/react/ChatProvider.tsx b/src/react/ChatProvider.tsx index 26189330..ef900661 100644 --- a/src/react/ChatProvider.tsx +++ b/src/react/ChatProvider.tsx @@ -1,10 +1,9 @@ import { useEffect, useState } from 'react' -import { Message } from '../chat' import { formatMessage } from '../botUtils' import { getBuiltinCommandsList, tryHandleBuiltinCommand } from '../builtinCommands' import { hideCurrentModal } from '../globalState' import { options } from '../optionsStorage' -import ChatComponent, { fadeMessage } from './ChatContainer' +import ChatComponent, { Message, fadeMessage } from './ChatContainer' import { useIsModalActive } from './utils' export default () => { From 6cd44ee82f4796fa46870d6595c4ecface6313e5 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Thu, 4 Jan 2024 01:30:31 +0530 Subject: [PATCH 0019/1316] wip working on collision shapes --- scripts/getMoreCollisionShapes.mjs | 79 ++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 scripts/getMoreCollisionShapes.mjs diff --git a/scripts/getMoreCollisionShapes.mjs b/scripts/getMoreCollisionShapes.mjs new file mode 100644 index 00000000..2e9e6b23 --- /dev/null +++ b/scripts/getMoreCollisionShapes.mjs @@ -0,0 +1,79 @@ +import minecraftData from 'minecraft-data' + +const latestData = minecraftData(minecraftData.supportedVersions.pc.at(-1)) + +// dont touch, these are the ones that are already full box +const fullBoxInteractionShapes = [ + 'dead_bush', + 'cave_vines_plant', + 'grass', + 'tall_seagrass', + 'spruce_sapling', + 'oak_sapling', + 'dark_oak_sapling', + 'birch_sapling', + 'seagrass', + 'nether_portal', + 'tall_grass', + 'lilac', +] + +// to fix +const fullBoxInteractionShapesTemp = [ + 'moving_piston', + 'lime_wall_banner', + 'gray_wall_banner', + 'weeping_vines_plant', + 'pumpkin_stem', + 'red_wall_banner', + 'crimson_wall_sign', + 'magenta_wall_banner', + 'melon_stem', + 'gray_banner', + 'spruce_sign', + 'pink_wall_banner', + 'purple_banner', + 'bamboo_sapling', + 'mangrove_sign', + 'cyan_banner', + 'blue_banner', + 'green_wall_banner', + 'yellow_banner', + 'black_wall_banner', + 'green_banner', + 'oak_sign', + 'jungle_sign', + 'yellow_wall_banner', + 'lime_banner', + 'tube_coral', + 'red_banner', + 'magenta_banner', + 'brown_wall_banner', + 'white_wall_banner', +] + +const fullShape = shapes.shapes[1] +const outputJson = {} + +const isNonInteractive = block => block.name.includes('air') || block.name.includes('water') || block.name.includes('lava') || block.name.includes('void') +const shapes = latestData.blockCollisionShapes; +const interestedBlocks = latestData.blocksArray.filter(block => { + const shapeId = shapes.blocks[block.name] + // console.log('shapeId', shapeId, block.name) + if (!shapeId) return true + const shape = typeof shapeId === 'number' ? shapes.shapes[shapeId] : shapeId + if (shape.length === 0) return true + // console.log(shape) +}).filter(b => !isNonInteractive(b)).filter(b => { + if (fullBoxInteractionShapes.includes(b.name)) { + outputJson[b.name] = fullShape + return false + } + return true +}).map(d => d.name) + +console.log(interestedBlocks) + +// read latest block states + +// read block model elements & combine From 9b6d7aee0df2d2e089b0936d31831fffe2d86a17 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Thu, 4 Jan 2024 02:06:26 +0530 Subject: [PATCH 0020/1316] add soundEvents --- src/soundEvents.ts | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/soundEvents.ts diff --git a/src/soundEvents.ts b/src/soundEvents.ts new file mode 100644 index 00000000..78a97c68 --- /dev/null +++ b/src/soundEvents.ts @@ -0,0 +1,36 @@ +import type { Block } from 'prismarine-block' + +let lastStepSound = 0 + +const getStepSound = (blockUnder: Block) => { + if (!blockUnder || blockUnder.type === 0) return + const soundsMap = window.allSoundsMap?.[bot.version] + if (!soundsMap) return + let soundResult = 'block.stone.step' + for (const x of Object.keys(soundsMap).map(n => n.split(';')[1])) { + const match = /block\.(.+)\.step/.exec(x) + const block = match?.[1] + if (!block) continue + if (loadedData.blocksByName[block]?.name === blockUnder.name) { + soundResult = x + break + } + } + return soundResult +} + +export const movementHappening = () => { + const THRESHOLD = 0.1 + const { x, z, y } = bot.player.entity.velocity + if (Math.abs(x) < THRESHOLD && (Math.abs(z) > THRESHOLD || Math.abs(y) > THRESHOLD)) { + // movement happening + if (Date.now() - lastStepSound > 500) { + const blockUnder = bot.world.getBlock(bot.entity.position.offset(0, -1, 0)) + const stepSound = getStepSound(blockUnder) + if (stepSound) { + playHardcodedSound(stepSound) + lastStepSound = Date.now() + } + } + } +} From 8dc5016d26346f2c5cbba15c796df6c86d6791bc Mon Sep 17 00:00:00 2001 From: Vitaly Date: Thu, 4 Jan 2024 05:08:53 +0530 Subject: [PATCH 0021/1316] =?UTF-8?q?feat:=20implement=20basic=20sound=20s?= =?UTF-8?q?ystem=20=F0=9F=94=8A=F0=9F=94=8A=F0=9F=94=8A!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/preview.yml | 2 + .github/workflows/publish.yml | 2 + prismarine-viewer/viewer/lib/viewer.ts | 30 ++- scripts/downloadSoundsMap.mjs | 7 + scripts/prepareSounds.mjs | 243 +++++++++++++++++++++++++ server.js | 2 + src/basicSounds.ts | 21 ++- src/globalState.ts | 2 +- src/globals.d.ts | 4 + src/guessProblem.ts | 2 +- src/index.ts | 6 + src/optionsGuiScheme.tsx | 7 +- src/optionsStorage.ts | 3 +- src/react/AppStatusProvider.tsx | 10 +- src/react/ChatContainer.css | 1 + src/react/SoundMuffler.tsx | 60 ++++++ src/reactUi.jsx | 2 + src/soundEvents.ts | 36 ---- src/soundSystem.ts | 229 +++++++++++++++++++++++ src/worldInteractions.js | 1 + 20 files changed, 621 insertions(+), 49 deletions(-) create mode 100644 scripts/downloadSoundsMap.mjs create mode 100644 scripts/prepareSounds.mjs create mode 100644 src/react/SoundMuffler.tsx delete mode 100644 src/soundEvents.ts create mode 100644 src/soundSystem.ts diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 52824004..cbd2f1a2 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -29,6 +29,8 @@ jobs: run: vercel build --token=${{ secrets.VERCEL_TOKEN }} - name: Copy playground files run: node prismarine-viewer/esbuild.mjs && cp prismarine-viewer/public/index.html .vercel/output/static/playground.html && cp prismarine-viewer/public/playground.js .vercel/output/static/playground.js + - name: Download Generated Sounds map + run: node scripts/downloadSoundsMap.mjs - name: Deploy Project Artifacts to Vercel uses: mathiasvr/command-output@v2.0.0 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2b84b90c..a52e6ab9 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,6 +21,8 @@ jobs: - run: vercel build --token=${{ secrets.VERCEL_TOKEN }} --prod - name: Copy playground files run: node prismarine-viewer/esbuild.mjs && cp prismarine-viewer/public/index.html .vercel/output/static/playground.html && cp prismarine-viewer/public/playground.js .vercel/output/static/playground.js + - name: Download Generated Sounds map + run: node scripts/downloadSoundsMap.mjs - name: Deploy Project to Vercel uses: mathiasvr/command-output@v2.0.0 with: diff --git a/prismarine-viewer/viewer/lib/viewer.ts b/prismarine-viewer/viewer/lib/viewer.ts index 92860fbf..56e45ad1 100644 --- a/prismarine-viewer/viewer/lib/viewer.ts +++ b/prismarine-viewer/viewer/lib/viewer.ts @@ -21,8 +21,9 @@ export class Viewer { isSneaking: boolean version: string cameraObjectOverride?: THREE.Object3D // for xr + audioListener: THREE.AudioListener - constructor (public renderer: THREE.WebGLRenderer, numWorkers?: number) { + constructor(public renderer: THREE.WebGLRenderer, numWorkers?: number) { this.scene = new THREE.Scene() this.scene.background = new THREE.Color('lightblue') @@ -91,6 +92,33 @@ export class Viewer { cam.rotation.set(pitch, yaw, roll, 'ZYX') } + playSound (position: Vec3, path: string, volume = 1) { + if (!this.audioListener) { + this.audioListener = new THREE.AudioListener() + this.camera.add(this.audioListener) + } + + const sound = new THREE.PositionalAudio(this.audioListener) + + const audioLoader = new THREE.AudioLoader() + let start = Date.now() + audioLoader.loadAsync(path).then((buffer) => { + if (Date.now() - start > 500) return + // play + sound.setBuffer(buffer) + sound.setRefDistance(20) + sound.setVolume(volume) + this.scene.add(sound) + // set sound position + sound.position.set(position.x, position.y, position.z) + sound.play() + sound.onEnded = () => { + this.scene.remove(sound) + sound.disconnect() + } + }) + } + // todo type listen (emitter) { emitter.on('entity', (e) => { diff --git a/scripts/downloadSoundsMap.mjs b/scripts/downloadSoundsMap.mjs new file mode 100644 index 00000000..391d5a1c --- /dev/null +++ b/scripts/downloadSoundsMap.mjs @@ -0,0 +1,7 @@ +import fs from 'fs' + +const url = 'https://github.com/zardoy/prismarine-web-client/raw/sounds-generated/sounds.js' +const savePath = 'dist/sounds.js' +fetch(url).then(res => res.text()).then(data => { + fs.writeFileSync(savePath, data, 'utf8') +}) diff --git a/scripts/prepareSounds.mjs b/scripts/prepareSounds.mjs new file mode 100644 index 00000000..4ed119cb --- /dev/null +++ b/scripts/prepareSounds.mjs @@ -0,0 +1,243 @@ +//@ts-check + +import { getVersionList, DEFAULT_RESOURCE_ROOT_URL } from '@xmcl/installer' +import path from 'path' +import fs from 'fs' +import { fileURLToPath } from 'url' +import { exec } from 'child_process' +import { promisify } from 'util' +import { build } from 'esbuild' + +const __dirname = path.dirname(fileURLToPath(new URL(import.meta.url))) + +const targetedVersions = ['1.20.1', '1.19.2', '1.18.2', '1.17.1', '1.16.5', '1.15.2', '1.14.4', '1.13.2', '1.12.2', '1.11.2', '1.10.2', '1.9.4', '1.8.9'] + +/** @type {{name, size, hash}[]} */ +let prevSounds = null + +const burgerDataUrl = (version) => `https://raw.githubusercontent.com/Pokechu22/Burger/gh-pages/${version}.json` +const burgerDataPath = './generated/burger.json' + +// const perVersionData: Record { + const { versions } = await getVersionList() + const lastVersion = versions.filter(version => !version.id.includes('w'))[0] + // if (lastVersion.id !== targetedVersions[0]) throw new Error('last version is not the same as targetedVersions[0], update') + for (const targetedVersion of targetedVersions) { + const versionData = versions.find(x => x.id === targetedVersion) + if (!versionData) throw new Error('no version data for ' + targetedVersion) + console.log('Getting assets for version', targetedVersion) + const { assetIndex } = await fetch(versionData.url).then((r) => r.json()) + /** @type {{objects: {[a: string]: { size, hash }}}} */ + const index = await fetch(assetIndex.url).then((r) => r.json()) + const soundAssets = Object.entries(index.objects).filter(([name]) => /* name.endsWith('.ogg') || */ name.startsWith('minecraft/sounds/')).map(([name, { size, hash }]) => ({ name, size, hash })) + soundAssets.sort((a, b) => a.name.localeCompare(b.name)) + if (prevSounds) { + const prevSoundNames = new Set(prevSounds.map(x => x.name)) + const addedSounds = prevSounds.filter(x => !soundAssets.some(y => y.name === x.name)) + // todo implement removed + const removedSounds = soundAssets.filter(x => !prevSoundNames.has(x.name)) + // console.log('+', addedSounds.map(x => x.name)) + // console.log('-', removedSounds.map(x => x.name)) + const changedSize = soundAssets.filter(x => prevSoundNames.has(x.name) && prevSounds.find(y => y.name === x.name).size !== x.size) + console.log('changed size', changedSize.map(x => ({ name: x.name, prev: prevSounds.find(y => y.name === x.name).size, curr: x.size }))) + if (addedSounds.length || changedSize.length) { + soundsPathVersionsRemap[targetedVersion] = [...addedSounds, ...changedSize].map(x => x.name.replace('minecraft/sounds/', '').replace('.ogg', '')) + } + if (addedSounds.length) { + console.log('downloading new sounds for version', targetedVersion) + downloadSounds(addedSounds, targetedVersion + '/') + } + if (changedSize.length) { + console.log('downloading changed sounds for version', targetedVersion) + downloadSounds(changedSize, targetedVersion + '/') + } + } else { + console.log('downloading sounds for version', targetedVersion) + downloadSounds(soundAssets) + } + prevSounds = soundAssets + } + async function downloadSound ({ name, hash, size }, namePath, log) { + const savePath = path.resolve(`generated/sounds/${namePath}`) + if (fs.existsSync(savePath)) { + // console.log('skipped', name) + return + } + log() + const r = await fetch(DEFAULT_RESOURCE_ROOT_URL + '/' + hash.slice(0, 2) + '/' + hash, /* {headers: {range: `bytes=0-${size-1}`}} */) + // save file + const file = await r.blob() + fs.mkdirSync(path.dirname(savePath), { recursive: true }) + await fs.promises.writeFile(savePath, Buffer.from(await file.arrayBuffer())) + + const reader = file.stream().getReader() + + const writer = fs.createWriteStream(savePath) + let offset = 0 + while (true) { + const { done, value } = await reader.read() + if (done) break + writer.write(Buffer.from(value)) + offset += value.byteLength + } + writer.close() + } + async function downloadSounds (assets, addPath = '') { + for (let i = 0; i < assets.length; i += 5) { + await Promise.all(assets.slice(i, i + 5).map((asset, j) => downloadSound(asset, `${addPath}${asset.name}`, () => { + console.log('downloading', addPath, asset.name, i + j, '/', assets.length) + }))) + } + } + + fs.writeFileSync('./generated/soundsPathVersionsRemap.json', JSON.stringify(soundsPathVersionsRemap), 'utf8') +} + +const lightpackOverrideSounds = { + 'Block breaking': 'step/stone1', + 'Block broken': 'dig/stone1', + 'Block placed': 'dig/stone1' +} + +// this is not done yet, will be used to select only sounds for bundle (most important ones) +const isSoundWhitelisted = (name) => name.startsWith('random/') || name.startsWith('note/') || name.endsWith('/say1') || name.endsWith('/death') || (name.startsWith('mob/') && name.endsWith('/step1')) || name.endsWith('/swoop1') || /* name.endsWith('/break1') || */ name.endsWith('dig/stone1') + +const ffmpeg = 'C:/Users/Vitaly/Documents/LosslessCut-win-x64/resources/ffmpeg.exe' // will be ffmpeg-static +const maintainBitrate = true + +const scanFilesDeep = async (root, onOggFile) => { + const files = await fs.promises.readdir(root, { withFileTypes: true }) + for (const file of files) { + if (file.isDirectory()) { + await scanFilesDeep(path.join(root, file.name), onOggFile) + } else if (file.name.endsWith('.ogg') && !files.some(x => x.name === file.name.replace('.ogg', '.mp3'))) { + await onOggFile(path.join(root, file.name)) + } + } +} + +const convertSounds = async () => { + const toConvert = [] + await scanFilesDeep('generated/sounds', (oggPath) => { + toConvert.push(oggPath) + }) + + const convertSound = async (i) => { + const proc = promisify(exec)(`${ffmpeg} -i "${toConvert[i]}" -y -codec:a libmp3lame ${maintainBitrate ? '-qscale:a 2' : ''} "${toConvert[i].replace('.ogg', '.mp3')}"`) + // pipe stdout to the console + proc.child.stdout.pipe(process.stdout) + await proc + console.log('converted to mp3', i, '/', toConvert.length, toConvert[i]) + } + + const CONCURRENCY = 5 + for(let i = 0; i < toConvert.length; i += CONCURRENCY) { + await Promise.all(toConvert.slice(i, i + CONCURRENCY).map((oggPath, j) => convertSound(i + j))) + } +} + +const getSoundsMap = (burgerData) => { + /** @type {Record} */ + return burgerData[0].sounds + // const map = JSON.parse(fs.readFileSync(burgerDataPath, 'utf8'))[0].sounds +} + +const writeSoundsMap = async () => { + // const burgerData = await fetch(burgerDataUrl(targetedVersions[0])).then((r) => r.json()) + // fs.writeFileSync(burgerDataPath, JSON.stringify(burgerData[0].sounds), 'utf8') + + const allSoundsMapOutput = {} + let prevMap + + // todo REMAP ONLY IDS. Do diffs, as mostly only ids are changed between versions + // const localTargetedVersions = targetedVersions.slice(0, 2) + const localTargetedVersions = targetedVersions + for (const targetedVersion of localTargetedVersions) { + const burgerData = await fetch(burgerDataUrl(targetedVersion)).then((r) => r.json()) + const allSoundsMap = getSoundsMap(burgerData) + // console.log(Object.keys(sounds).length, 'ids') + const outputIdMap = {} + const outputFilesMap = {} + + const classes = {} + let keysStats = { + new: 0, + same: 0 + } + for (const { id, subtitle, sounds, name } of Object.values(allSoundsMap)) { + if (!sounds?.length /* && !subtitle */) continue + const firstName = sounds[0].name + // const includeSound = isSoundWhitelisted(firstName) + // if (!includeSound) continue + const mostUsedSound = sounds.sort((a, b) => b.weight - a.weight)[0] + const targetSound = sounds[0] + // outputMap[id] = { subtitle, sounds: mostUsedSound } + // outputMap[id] = { subtitle, sounds } + const soundFilePath = `generated/sounds/minecraft/sounds/${targetSound.name}.mp3` + // if (!fs.existsSync(soundFilePath)) { + // console.warn('no sound file', targetSound.name) + // continue + // } + const key = `${id};${name}` + outputIdMap[key] = `${targetSound.volume ?? 1};${targetSound.name}` + if (prevMap && prevMap[key]) { + keysStats.same++ + } else { + keysStats.new++ + } + // for (const {name: soundName} of sounds ?? []) { + // let obj = classes + // for (const part of soundName.split('/')) { + // obj[part] ??= {} + // obj = obj[part] + // } + // } + } + // console.log(classes) + // console.log('to download', new Set(Object.values(outputIdMap).flatMap(x => x.sounds)).size) + // console.log('json size', JSON.stringify(outputIdMap).length / 1024 / 1024) + allSoundsMapOutput[targetedVersion] = outputIdMap + prevMap = outputIdMap + // const allSoundNames = new Set(Object.values(allSoundsMap).flatMap(({ name, sounds }) => { + // if (!sounds) { + // console.log(name) + // return [] + // } + // return sounds.map(sound => sound.name) + // })) + // console.log(allSoundNames.size, 'sounds') + } + + fs.writeFileSync('./generated/sounds.json', JSON.stringify(allSoundsMapOutput), 'utf8') +} + +const makeSoundsBundle = async () => { + const allSoundsMap = JSON.parse(fs.readFileSync('./generated/sounds.json', 'utf8')) + const allSoundsVersionedMap = JSON.parse(fs.readFileSync('./generated/soundsPathVersionsRemap.json', 'utf8')) + + const allSoundsMeta = { + format: 'mp3', + baseUrl: 'https://raw.githubusercontent.com/zardoy/prismarine-web-client/sounds-generated/sounds/' + } + + await build({ + bundle: true, + outfile: `dist/sounds.js`, + stdin: { + contents: `window.allSoundsMap = ${JSON.stringify(allSoundsMap)}\nwindow.allSoundsVersionedMap = ${JSON.stringify(allSoundsVersionedMap)}\nwindow.allSoundsMeta = ${JSON.stringify(allSoundsMeta)}`, + resolveDir: __dirname, + sourcefile: `sounds.js`, + loader: 'js', + }, + metafile: true, + }) +} + +// downloadAllSounds() +// convertSounds() +// writeSoundsMap() +// makeSoundsBundle() diff --git a/server.js b/server.js index 0f3b1ca2..b6dd4135 100644 --- a/server.js +++ b/server.js @@ -17,6 +17,8 @@ app.use(netApi({ allowOrigin: '*' })) if (!isProd) { app.use('/blocksStates', express.static(path.join(__dirname, './prismarine-viewer/public/blocksStates'))) app.use('/textures', express.static(path.join(__dirname, './prismarine-viewer/public/textures'))) + + app.use('/sounds', express.static(path.join(__dirname, './generated/sounds/'))) } // patch config app.get('/config.json', (req, res, next) => { diff --git a/src/basicSounds.ts b/src/basicSounds.ts index 2a084692..40b6963e 100644 --- a/src/basicSounds.ts +++ b/src/basicSounds.ts @@ -7,7 +7,7 @@ const sounds: Record = {} const loadingSounds = [] as string[] const convertedSounds = [] as string[] export async function loadSound (path: string) { - if (loadingSounds.includes(path)) return + if (loadingSounds.includes(path)) return true loadingSounds.push(path) const res = await window.fetch(path) const data = await res.arrayBuffer() @@ -16,8 +16,19 @@ export async function loadSound (path: string) { loadingSounds.splice(loadingSounds.indexOf(path), 1) } -export async function playSound (path) { - const volume = options.volume / 100 +export const loadOrPlaySound = async (url, soundVolume = 1) => { + const soundBuffer = sounds[url] + if (!soundBuffer) { + const start = Date.now() + const cancelled = await loadSound(url) + if (cancelled || Date.now() - start > 500) return + } + + await playSound(url) +} + +export async function playSound (url, soundVolume = 1) { + const volume = soundVolume * (options.volume / 100) if (!volume) return @@ -29,9 +40,9 @@ export async function playSound (path) { convertedSounds.push(soundName) } - const soundBuffer = sounds[path] + const soundBuffer = sounds[url] if (!soundBuffer) { - console.warn(`Sound ${path} not loaded`) + console.warn(`Sound ${url} not loaded yet`) return } diff --git a/src/globalState.ts b/src/globalState.ts index 92bbb5c1..e9391c83 100644 --- a/src/globalState.ts +++ b/src/globalState.ts @@ -180,7 +180,7 @@ const initialNotification = { } export const notification = proxy(initialNotification) -export const showNotification = (/** @type {Partial} */newNotification) => { +export const showNotification = (newNotification: Partial) => { Object.assign(notification, { show: true, ...newNotification }, initialNotification) } diff --git a/src/globals.d.ts b/src/globals.d.ts index 0a83643c..e7753d1a 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -10,6 +10,10 @@ declare const localServer: import('flying-squid/dist/types').FullServer & { opti /** all currently loaded mc data */ declare const mcData: Record declare const loadedData: import('minecraft-data').IndexedData +declare const customEvents: import('typed-emitter').default<{ + singleplayer (): void + digStart() +}> declare interface Document { getElementById (id): any diff --git a/src/guessProblem.ts b/src/guessProblem.ts index 2ebdbfa6..ecc7dbf1 100644 --- a/src/guessProblem.ts +++ b/src/guessProblem.ts @@ -1,4 +1,4 @@ -export const guessProblem = (/** @type {string} */errorMessage) => { +export const guessProblem = (errorMessage: string) => { if (errorMessage.endsWith('Socket error: ECONNREFUSED')) { return 'Most probably the server is not running.' } diff --git a/src/index.ts b/src/index.ts index 0a1c2285..cb5d44d4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -82,8 +82,13 @@ import { fsState } from './loadSave' import { watchFov } from './rendererUtils' import { loadInMemorySave } from './react/SingleplayerProvider' +// side effects +import { downloadSoundsIfNeeded } from './soundSystem' +import EventEmitter from 'events' + window.debug = debug window.THREE = THREE +window.customEvents = new EventEmitter() // ACTUAL CODE @@ -342,6 +347,7 @@ async function connect (connectOptions: { Object.assign(serverOptions, connectOptions.serverOverridesFlat ?? {}) const downloadMcData = async (version: string) => { setLoadingScreenStatus(`Downloading data for ${version}`) + await downloadSoundsIfNeeded() await loadScript(`./mc-data/${toMajorVersion(version)}.js`) miscUiState.loadedDataVersion = version try { diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx index 5ee73a83..07242f37 100644 --- a/src/optionsGuiScheme.tsx +++ b/src/optionsGuiScheme.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' import { useSnapshot } from 'valtio' -import { miscUiState, openOptionsMenu } from './globalState' +import { miscUiState, openOptionsMenu, showModal } from './globalState' import { openURL } from './menus/components/common' import { AppOptions, options } from './optionsStorage' import Button from './react/Button' @@ -181,6 +181,11 @@ export const guiOptionsScheme: { ], sound: [ { volume: {} }, + { + custom () { + return + +} + +export default () => { + const isModalActive = useIsModalActive('sound-muffler') + + const [showMuted, setShowMuted] = useState(true) + const [i, setI] = useState(0) + const { mutedSounds } = useSnapshot(options) + + if (!isModalActive) return null + + return +
    + + + Last World Played + {Object.entries(lastPlayedSounds.lastServerPlayed).map(([key, value]) => { + if (!showMuted && mutedSounds.includes(key)) return null as never + return [key, value.count] as const + }).filter(Boolean).sort((a, b) => b[1] - a[1]).slice(0, 20).map(([key, count]) => { + return + {count} + + })} + Last Client Played + {lastPlayedSounds.lastClientPlayed.map((key) => { + if (!showMuted && mutedSounds.includes(key)) return null as never + return + })} + +
    +
    +} diff --git a/src/reactUi.jsx b/src/reactUi.jsx index 77139bb4..6e7d2f9b 100644 --- a/src/reactUi.jsx +++ b/src/reactUi.jsx @@ -18,6 +18,7 @@ import AppStatusProvider from './react/AppStatusProvider' import SelectOption from './react/SelectOption' import EnterFullscreenButton from './react/EnterFullscreenButton' import ChatProvider from './react/ChatProvider' +import SoundMuffler from './react/SoundMuffler' // todo useInterfaceState.setState({ @@ -127,6 +128,7 @@ const InGameUi = () => { {/* apply scaling */} +
    diff --git a/src/soundEvents.ts b/src/soundEvents.ts deleted file mode 100644 index 78a97c68..00000000 --- a/src/soundEvents.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { Block } from 'prismarine-block' - -let lastStepSound = 0 - -const getStepSound = (blockUnder: Block) => { - if (!blockUnder || blockUnder.type === 0) return - const soundsMap = window.allSoundsMap?.[bot.version] - if (!soundsMap) return - let soundResult = 'block.stone.step' - for (const x of Object.keys(soundsMap).map(n => n.split(';')[1])) { - const match = /block\.(.+)\.step/.exec(x) - const block = match?.[1] - if (!block) continue - if (loadedData.blocksByName[block]?.name === blockUnder.name) { - soundResult = x - break - } - } - return soundResult -} - -export const movementHappening = () => { - const THRESHOLD = 0.1 - const { x, z, y } = bot.player.entity.velocity - if (Math.abs(x) < THRESHOLD && (Math.abs(z) > THRESHOLD || Math.abs(y) > THRESHOLD)) { - // movement happening - if (Date.now() - lastStepSound > 500) { - const blockUnder = bot.world.getBlock(bot.entity.position.offset(0, -1, 0)) - const stepSound = getStepSound(blockUnder) - if (stepSound) { - playHardcodedSound(stepSound) - lastStepSound = Date.now() - } - } - } -} diff --git a/src/soundSystem.ts b/src/soundSystem.ts new file mode 100644 index 00000000..f1604228 --- /dev/null +++ b/src/soundSystem.ts @@ -0,0 +1,229 @@ +import { subscribeKey } from 'valtio/utils' +import { Vec3 } from 'vec3' +import { versionToNumber } from 'prismarine-viewer/viewer/prepare/utils' +import { loadScript } from 'prismarine-viewer/viewer/lib/utils' +import type { Block } from 'prismarine-block' +import { miscUiState, showNotification } from './globalState' +import { options } from './optionsStorage' +import { loadOrPlaySound } from './basicSounds' + +subscribeKey(miscUiState, 'gameLoaded', async () => { + const soundsLegacyMap = window.allSoundsVersionedMap as Record + const allSoundsMap = window.allSoundsMap as Record> + const allSoundsMeta = window.allSoundsMeta as { format: string, baseUrl: string } + if (!allSoundsMap) { + return + } + + // todo also use major versioned hardcoded sounds + const soundsMap = allSoundsMap[bot.version] + + if (!soundsMap) { + console.warn('No sounds map for version', bot.version, 'supported versions are', Object.keys(allSoundsMap).join(', ')) + showNotification({ + message: 'No sounds map for version ' + bot.version, + }) + return + } + + if (!miscUiState.gameLoaded || !soundsMap) { + return + } + + const soundsPerId = Object.fromEntries(Object.entries(soundsMap).map(([id, sound]) => [+id.split(';')[0], sound])) + const soundsPerName = Object.fromEntries(Object.entries(soundsMap).map(([id, sound]) => [id.split(';')[1], sound])) + const soundIdToName = Object.fromEntries(Object.entries(soundsMap).map(([id, sound]) => [+id.split(';')[0], id.split(';')[1]])) + + const playGeneralSound = async (soundId: string, soundString: string | undefined, position?: Vec3, volume = 1, pitch?: number) => { + if (!soundString) { + console.warn('Unknown sound received from server to play', soundId) + return + } + if (!options.volume) return + console.debug('play sound', soundId) + const parts = soundString.split(';') + const soundVolume = +parts[0]! + const soundName = parts[1]! + // console.log('pitch', pitch) + const versionedSound = getVersionedSound(bot.version, soundName, Object.entries(soundsLegacyMap)) + // todo test versionedSound + const url = allSoundsMeta.baseUrl.replace(/\/$/, '') + (versionedSound ? `/${versionedSound}` : '') + '/minecraft/sounds/' + soundName + '.' + allSoundsMeta.format + const soundKey = soundIdToName[+soundId] ?? soundId + const isMuted = options.mutedSounds.includes(soundKey) + if (position) { + if (!isMuted) { + viewer.playSound(position, url, soundVolume * volume * (options.volume / 100)) + } + if (getDistance(bot.entity.position, position) < 16) { + lastPlayedSounds.lastServerPlayed[soundKey] ??= { count: 0, last: 0 } + lastPlayedSounds.lastServerPlayed[soundKey].count++ + lastPlayedSounds.lastServerPlayed[soundKey].last = Date.now() + } + } else { + if (!isMuted) { + await loadOrPlaySound(url, volume) + } + lastPlayedSounds.lastClientPlayed.push(soundKey) + if (lastPlayedSounds.lastClientPlayed.length > 10) { + lastPlayedSounds.lastClientPlayed.shift() + } + } + } + const playHardcodedSound = async (soundId: string, position?: Vec3, volume = 1, pitch?: number) => { + const sound = soundsPerName[soundId] + await playGeneralSound(soundId, sound, position, volume, pitch) + } + bot.on('soundEffectHeard', async (soundId, position, volume, pitch) => { + console.debug('soundEffectHeard', soundId, volume) + await playHardcodedSound(soundId, position, volume, pitch) + }) + bot.on('hardcodedSoundEffectHeard', async (soundId, soundCategory, position, volume, pitch) => { + const sound = soundsPerId[soundId] + await playGeneralSound(soundId.toString(), sound, position, volume, pitch) + }) + bot.on('entityHurt', async (entity) => { + if (entity.id === bot.entity.id) { + await playHardcodedSound('entity.player.hurt') + } + }) + + const useBlockSound = (blockName: string, category: string, fallback: string) => { + blockName = { + // todo somehow generated, not full + grass_block: 'grass', + tall_grass: 'grass', + fern: 'grass', + large_fern: 'grass', + dead_bush: 'grass', + seagrass: 'grass', + tall_seagrass: 'grass', + kelp: 'grass', + kelp_plant: 'grass', + sugar_cane: 'grass', + bamboo: 'grass', + vine: 'grass', + nether_sprouts: 'grass', + nether_wart: 'grass', + twisting_vines: 'grass', + weeping_vines: 'grass', + + cobblestone: 'stone', + stone_bricks: 'stone', + mossy_stone_bricks: 'stone', + cracked_stone_bricks: 'stone', + chiseled_stone_bricks: 'stone', + stone_brick_slab: 'stone', + stone_brick_stairs: 'stone', + stone_brick_wall: 'stone', + polished_granite: 'stone', + }[blockName] ?? blockName + const key = 'block.' + blockName + '.' + category + return soundsPerName[key] ? key : fallback + } + + const getStepSound = (blockUnder: Block) => { + // const soundsMap = window.allSoundsMap?.[bot.version] + // if (!soundsMap) return + // let soundResult = 'block.stone.step' + // for (const x of Object.keys(soundsMap).map(n => n.split(';')[1])) { + // const match = /block\.(.+)\.step/.exec(x) + // const block = match?.[1] + // if (!block) continue + // if (loadedData.blocksByName[block]?.name === blockUnder.name) { + // soundResult = x + // break + // } + // } + return useBlockSound(blockUnder.name, 'step', 'block.stone.step') + } + + let lastStepSound = 0 + const movementHappening = async () => { + const VELOCITY_THRESHOLD = 0.1 + const { x, z, y } = bot.player.entity.velocity + if (bot.entity.onGround && Math.abs(x) < VELOCITY_THRESHOLD && (Math.abs(z) > VELOCITY_THRESHOLD || Math.abs(y) > VELOCITY_THRESHOLD)) { + // movement happening + if (Date.now() - lastStepSound > 300) { + const blockUnder = bot.world.getBlock(bot.entity.position.offset(0, -1, 0)) + const stepSound = getStepSound(blockUnder) + if (stepSound) { + await playHardcodedSound(stepSound, undefined, 0.6)// todo not sure why 0.6 + lastStepSound = Date.now() + } + } + } + } + + const playBlockBreak = async (blockName: string, position?: Vec3) => { + const sound = useBlockSound(blockName, 'break', 'block.stone.break') + + await playHardcodedSound(sound, position, 0.6, 1) + } + + const registerEvents = () => { + bot.on('move', () => { + void movementHappening() + }) + bot._client.on('world_event', async ({ effectId, location, data, global:disablePosVolume }) => { + const position = disablePosVolume ? undefined : new Vec3(location.x, location.y, location.z) + if (effectId === 2001) { + // break event + const block = loadedData.blocksByStateId[data] + await playBlockBreak(block.name, position) + } + // these produce glass break sound + if (effectId === 2002 || effectId === 2003 || effectId === 2007) { + await playHardcodedSound('block.glass.break', position, 1, 1) + } + }) + let diggingBlock: Block | null = null + customEvents.on('digStart', () => { + diggingBlock = bot.blockAtCursor(5) + }) + bot.on('diggingCompleted', async () => { + if (diggingBlock) { + await playBlockBreak(diggingBlock.name) + } + }) + } + + registerEvents() + + console.log(getVersionedSound('1.13', 'mob/fox/spit1', Object.entries(soundsLegacyMap))) + // 1.20+ soundEffectHeard is broken atm + // bot._client.on('packet', (data, { name }, buffer) => { + // if (name === 'sound_effect') { + // console.log(data, buffer) + // } + // }) +}) + +const getVersionedSound = (version: string, item: string, itemsMapSortedEntries: Array<[string, string[]]>) => { + const verNumber = versionToNumber(version) + for (const [itemsVer, items] of itemsMapSortedEntries) { + // 1.18 < 1.18.1 + // 1.13 < 1.13.2 + if (items.includes(item) && verNumber <= versionToNumber(itemsVer)) { + return itemsVer + } + } +} + +export const downloadSoundsIfNeeded = async () => { + if (!window.allSoundsMap) { + try { + await loadScript('./sounds.js') + } catch (err) { + console.warn('Sounds map was not generated. Sounds will not be played.') + } + } +} + +export const lastPlayedSounds = { + lastClientPlayed: [] as string[], + lastServerPlayed: {} as Record, +} + +const getDistance = (pos1: Vec3, pos2: Vec3) => { + return Math.hypot((pos1.x - pos2.x), (pos1.y - pos2.y), (pos1.z - pos2.z)) +} diff --git a/src/worldInteractions.js b/src/worldInteractions.js index 45622060..c86680f7 100644 --- a/src/worldInteractions.js +++ b/src/worldInteractions.js @@ -195,6 +195,7 @@ class WorldInteraction { if (err.message === 'Digging aborted') return throw err }) + customEvents.emit('digStart') this.lastDigged = Date.now() } this.prevOnGround = onGround From b7bfe5d4d8a78eb5bce4fadf6e4784854f0056b1 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Thu, 4 Jan 2024 05:09:25 +0530 Subject: [PATCH 0022/1316] fix: fix compatibility with play.minemalia.com --- src/menus/components/bossbars_overlay.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/menus/components/bossbars_overlay.js b/src/menus/components/bossbars_overlay.js index 5c22fa9a..015dc6db 100644 --- a/src/menus/components/bossbars_overlay.js +++ b/src/menus/components/bossbars_overlay.js @@ -69,8 +69,8 @@ class BossBar extends LitElement { } setTitle (bar) { - if (bar._title.text) this.title = bar._title.text - else this.title = translations[this._title.translate] || 'Unknown Entity' + if (bar._title.text) this.title = bar.title.text + else this.title = translations[bar.title.translate] || 'Unknown Entity' } setColor (bar) { From 8e95588e230b23cc370910de73c1b5fd2a3e024e Mon Sep 17 00:00:00 2001 From: Vitaly Date: Thu, 4 Jan 2024 05:21:02 +0530 Subject: [PATCH 0023/1316] up tsx --- pnpm-lock.yaml | 258 +++++++++++++++++++++++++++++++-- prismarine-viewer/package.json | 2 +- 2 files changed, 249 insertions(+), 11 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10f7c83d..c831da68 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -340,8 +340,8 @@ importers: specifier: ^1.3.0 version: 1.4.0 tsx: - specifier: ^3.13.0 - version: 3.14.0 + specifier: ^4.7.0 + version: 4.7.0 vec3: specifier: ^0.1.7 version: 0.1.8 @@ -1883,6 +1883,15 @@ packages: resolution: {integrity: sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==} dev: false + /@esbuild/aix-ppc64@0.19.11: + resolution: {integrity: sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: false + optional: true + /@esbuild/android-arm64@0.18.20: resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} engines: {node: '>=12'} @@ -1891,6 +1900,15 @@ packages: requiresBuild: true optional: true + /@esbuild/android-arm64@0.19.11: + resolution: {integrity: sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: false + optional: true + /@esbuild/android-arm64@0.19.3: resolution: {integrity: sha512-w+Akc0vv5leog550kjJV9Ru+MXMR2VuMrui3C61mnysim0gkFCPOUTAfzTP0qX+HpN9Syu3YA3p1hf3EPqObRw==} engines: {node: '>=12'} @@ -1908,6 +1926,15 @@ packages: requiresBuild: true optional: true + /@esbuild/android-arm@0.19.11: + resolution: {integrity: sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: false + optional: true + /@esbuild/android-arm@0.19.3: resolution: {integrity: sha512-Lemgw4io4VZl9GHJmjiBGzQ7ONXRfRPHcUEerndjwiSkbxzrpq0Uggku5MxxrXdwJ+pTj1qyw4jwTu7hkPsgIA==} engines: {node: '>=12'} @@ -1925,6 +1952,15 @@ packages: requiresBuild: true optional: true + /@esbuild/android-x64@0.19.11: + resolution: {integrity: sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: false + optional: true + /@esbuild/android-x64@0.19.3: resolution: {integrity: sha512-FKQJKkK5MXcBHoNZMDNUAg1+WcZlV/cuXrWCoGF/TvdRiYS4znA0m5Il5idUwfxrE20bG/vU1Cr5e1AD6IEIjQ==} engines: {node: '>=12'} @@ -1942,6 +1978,15 @@ packages: requiresBuild: true optional: true + /@esbuild/darwin-arm64@0.19.11: + resolution: {integrity: sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + /@esbuild/darwin-arm64@0.19.3: resolution: {integrity: sha512-kw7e3FXU+VsJSSSl2nMKvACYlwtvZB8RUIeVShIEY6PVnuZ3c9+L9lWB2nWeeKWNNYDdtL19foCQ0ZyUL7nqGw==} engines: {node: '>=12'} @@ -1959,6 +2004,15 @@ packages: requiresBuild: true optional: true + /@esbuild/darwin-x64@0.19.11: + resolution: {integrity: sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + /@esbuild/darwin-x64@0.19.3: resolution: {integrity: sha512-tPfZiwF9rO0jW6Jh9ipi58N5ZLoSjdxXeSrAYypy4psA2Yl1dAMhM71KxVfmjZhJmxRjSnb29YlRXXhh3GqzYw==} engines: {node: '>=12'} @@ -1976,6 +2030,15 @@ packages: requiresBuild: true optional: true + /@esbuild/freebsd-arm64@0.19.11: + resolution: {integrity: sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: false + optional: true + /@esbuild/freebsd-arm64@0.19.3: resolution: {integrity: sha512-ERDyjOgYeKe0Vrlr1iLrqTByB026YLPzTytDTz1DRCYM+JI92Dw2dbpRHYmdqn6VBnQ9Bor6J8ZlNwdZdxjlSg==} engines: {node: '>=12'} @@ -1993,6 +2056,15 @@ packages: requiresBuild: true optional: true + /@esbuild/freebsd-x64@0.19.11: + resolution: {integrity: sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: false + optional: true + /@esbuild/freebsd-x64@0.19.3: resolution: {integrity: sha512-nXesBZ2Ad1qL+Rm3crN7NmEVJ5uvfLFPLJev3x1j3feCQXfAhoYrojC681RhpdOph8NsvKBBwpYZHR7W0ifTTA==} engines: {node: '>=12'} @@ -2010,6 +2082,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-arm64@0.19.11: + resolution: {integrity: sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-arm64@0.19.3: resolution: {integrity: sha512-qXvYKmXj8GcJgWq3aGvxL/JG1ZM3UR272SdPU4QSTzD0eymrM7leiZH77pvY3UetCy0k1xuXZ+VPvoJNdtrsWQ==} engines: {node: '>=12'} @@ -2027,6 +2108,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-arm@0.19.11: + resolution: {integrity: sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-arm@0.19.3: resolution: {integrity: sha512-zr48Cg/8zkzZCzDHNxXO/89bf9e+r4HtzNUPoz4GmgAkF1gFAFmfgOdCbR8zMbzFDGb1FqBBhdXUpcTQRYS1cQ==} engines: {node: '>=12'} @@ -2044,6 +2134,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-ia32@0.19.11: + resolution: {integrity: sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-ia32@0.19.3: resolution: {integrity: sha512-7XlCKCA0nWcbvYpusARWkFjRQNWNGlt45S+Q18UeS///K6Aw8bB2FKYe9mhVWy/XLShvCweOLZPrnMswIaDXQA==} engines: {node: '>=12'} @@ -2061,6 +2160,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-loong64@0.19.11: + resolution: {integrity: sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-loong64@0.19.3: resolution: {integrity: sha512-qGTgjweER5xqweiWtUIDl9OKz338EQqCwbS9c2Bh5jgEH19xQ1yhgGPNesugmDFq+UUSDtWgZ264st26b3de8A==} engines: {node: '>=12'} @@ -2078,6 +2186,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-mips64el@0.19.11: + resolution: {integrity: sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-mips64el@0.19.3: resolution: {integrity: sha512-gy1bFskwEyxVMFRNYSvBauDIWNggD6pyxUksc0MV9UOBD138dKTzr8XnM2R4mBsHwVzeuIH8X5JhmNs2Pzrx+A==} engines: {node: '>=12'} @@ -2095,6 +2212,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-ppc64@0.19.11: + resolution: {integrity: sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-ppc64@0.19.3: resolution: {integrity: sha512-UrYLFu62x1MmmIe85rpR3qou92wB9lEXluwMB/STDzPF9k8mi/9UvNsG07Tt9AqwPQXluMQ6bZbTzYt01+Ue5g==} engines: {node: '>=12'} @@ -2112,6 +2238,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-riscv64@0.19.11: + resolution: {integrity: sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-riscv64@0.19.3: resolution: {integrity: sha512-9E73TfyMCbE+1AwFOg3glnzZ5fBAFK4aawssvuMgCRqCYzE0ylVxxzjEfut8xjmKkR320BEoMui4o/t9KA96gA==} engines: {node: '>=12'} @@ -2129,6 +2264,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-s390x@0.19.11: + resolution: {integrity: sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-s390x@0.19.3: resolution: {integrity: sha512-LlmsbuBdm1/D66TJ3HW6URY8wO6IlYHf+ChOUz8SUAjVTuaisfuwCOAgcxo3Zsu3BZGxmI7yt//yGOxV+lHcEA==} engines: {node: '>=12'} @@ -2146,6 +2290,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-x64@0.19.11: + resolution: {integrity: sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-x64@0.19.3: resolution: {integrity: sha512-ogV0+GwEmvwg/8ZbsyfkYGaLACBQWDvO0Kkh8LKBGKj9Ru8VM39zssrnu9Sxn1wbapA2qNS6BiLdwJZGouyCwQ==} engines: {node: '>=12'} @@ -2163,6 +2316,15 @@ packages: requiresBuild: true optional: true + /@esbuild/netbsd-x64@0.19.11: + resolution: {integrity: sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: false + optional: true + /@esbuild/netbsd-x64@0.19.3: resolution: {integrity: sha512-o1jLNe4uzQv2DKXMlmEzf66Wd8MoIhLNO2nlQBHLtWyh2MitDG7sMpfCO3NTcoTMuqHjfufgUQDFRI5C+xsXQw==} engines: {node: '>=12'} @@ -2180,6 +2342,15 @@ packages: requiresBuild: true optional: true + /@esbuild/openbsd-x64@0.19.11: + resolution: {integrity: sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: false + optional: true + /@esbuild/openbsd-x64@0.19.3: resolution: {integrity: sha512-AZJCnr5CZgZOdhouLcfRdnk9Zv6HbaBxjcyhq0StNcvAdVZJSKIdOiPB9az2zc06ywl0ePYJz60CjdKsQacp5Q==} engines: {node: '>=12'} @@ -2197,6 +2368,15 @@ packages: requiresBuild: true optional: true + /@esbuild/sunos-x64@0.19.11: + resolution: {integrity: sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: false + optional: true + /@esbuild/sunos-x64@0.19.3: resolution: {integrity: sha512-Acsujgeqg9InR4glTRvLKGZ+1HMtDm94ehTIHKhJjFpgVzZG9/pIcWW/HA/DoMfEyXmANLDuDZ2sNrWcjq1lxw==} engines: {node: '>=12'} @@ -2214,6 +2394,15 @@ packages: requiresBuild: true optional: true + /@esbuild/win32-arm64@0.19.11: + resolution: {integrity: sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@esbuild/win32-arm64@0.19.3: resolution: {integrity: sha512-FSrAfjVVy7TifFgYgliiJOyYynhQmqgPj15pzLyJk8BUsnlWNwP/IAy6GAiB1LqtoivowRgidZsfpoYLZH586A==} engines: {node: '>=12'} @@ -2231,6 +2420,15 @@ packages: requiresBuild: true optional: true + /@esbuild/win32-ia32@0.19.11: + resolution: {integrity: sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@esbuild/win32-ia32@0.19.3: resolution: {integrity: sha512-xTScXYi12xLOWZ/sc5RBmMN99BcXp/eEf7scUC0oeiRoiT5Vvo9AycuqCp+xdpDyAU+LkrCqEpUS9fCSZF8J3Q==} engines: {node: '>=12'} @@ -2248,6 +2446,15 @@ packages: requiresBuild: true optional: true + /@esbuild/win32-x64@0.19.11: + resolution: {integrity: sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@esbuild/win32-x64@0.19.3: resolution: {integrity: sha512-FbUN+0ZRXsypPyWE2IwIkVjDkDnJoMJARWOcFZn4KPPli+QnKqF0z1anvfaYe3ev5HFCpRDLLBDHyOALLppWHw==} engines: {node: '>=12'} @@ -7573,6 +7780,37 @@ packages: '@esbuild/win32-ia32': 0.18.20 '@esbuild/win32-x64': 0.18.20 + /esbuild@0.19.11: + resolution: {integrity: sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.19.11 + '@esbuild/android-arm': 0.19.11 + '@esbuild/android-arm64': 0.19.11 + '@esbuild/android-x64': 0.19.11 + '@esbuild/darwin-arm64': 0.19.11 + '@esbuild/darwin-x64': 0.19.11 + '@esbuild/freebsd-arm64': 0.19.11 + '@esbuild/freebsd-x64': 0.19.11 + '@esbuild/linux-arm': 0.19.11 + '@esbuild/linux-arm64': 0.19.11 + '@esbuild/linux-ia32': 0.19.11 + '@esbuild/linux-loong64': 0.19.11 + '@esbuild/linux-mips64el': 0.19.11 + '@esbuild/linux-ppc64': 0.19.11 + '@esbuild/linux-riscv64': 0.19.11 + '@esbuild/linux-s390x': 0.19.11 + '@esbuild/linux-x64': 0.19.11 + '@esbuild/netbsd-x64': 0.19.11 + '@esbuild/openbsd-x64': 0.19.11 + '@esbuild/sunos-x64': 0.19.11 + '@esbuild/win32-arm64': 0.19.11 + '@esbuild/win32-ia32': 0.19.11 + '@esbuild/win32-x64': 0.19.11 + dev: false + /esbuild@0.19.3: resolution: {integrity: sha512-UlJ1qUUA2jL2nNib1JTSkifQTcYTroFqRjwCFW4QYEKEsixXD5Tik9xML7zh2gTxkYTBKGHNH9y7txMwVyPbjw==} engines: {node: '>=12'} @@ -13444,13 +13682,13 @@ packages: /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - /tsx@3.14.0: - resolution: {integrity: sha512-xHtFaKtHxM9LOklMmJdI3BEnQq/D5F73Of2E1GDrITi9sgoVkvIsrQUTY1G8FlmGtA+awCI4EBlTRRYxkL2sRg==} + /tsx@4.7.0: + resolution: {integrity: sha512-I+t79RYPlEYlHn9a+KzwrvEwhJg35h/1zHsLC2JXvhC2mdynMv6Zxzvhv5EMV6VF5qJlLlkSnMVvdZV3PSIGcg==} + engines: {node: '>=18.0.0'} hasBin: true dependencies: - esbuild: 0.18.20 + esbuild: 0.19.11 get-tsconfig: 4.7.2 - source-map-support: 0.5.21 optionalDependencies: fsevents: 2.3.3 dev: false @@ -14646,8 +14884,8 @@ packages: - react dev: true - github.com/zardoy/minecraft-protocol/8e61798aeae786db3ddd4bbd9e19b6e30bb2520c: - resolution: {tarball: https://codeload.github.com/zardoy/minecraft-protocol/tar.gz/8e61798aeae786db3ddd4bbd9e19b6e30bb2520c} + github.com/zardoy/minecraft-protocol/436e0f2945d82408cfd1eb4262535c205bcba8d0: + resolution: {tarball: https://codeload.github.com/zardoy/minecraft-protocol/tar.gz/436e0f2945d82408cfd1eb4262535c205bcba8d0} name: minecraft-protocol version: 1.44.0 engines: {node: '>=14'} @@ -14681,7 +14919,7 @@ packages: engines: {node: '>=14'} dependencies: minecraft-data: 3.48.0 - minecraft-protocol: github.com/zardoy/minecraft-protocol/8e61798aeae786db3ddd4bbd9e19b6e30bb2520c + minecraft-protocol: github.com/zardoy/minecraft-protocol/436e0f2945d82408cfd1eb4262535c205bcba8d0 prismarine-biome: 1.3.0(minecraft-data@3.48.0)(prismarine-registry@1.7.0) prismarine-block: github.com/zardoy/prismarine-block/753cf1fe507f7647063c69d5c124d40f85b29cc2 prismarine-chat: 1.9.1 @@ -14767,7 +15005,7 @@ packages: flatmap: 0.0.3 long: 5.2.3 minecraft-data: 3.48.0 - minecraft-protocol: github.com/zardoy/minecraft-protocol/8e61798aeae786db3ddd4bbd9e19b6e30bb2520c + minecraft-protocol: github.com/zardoy/minecraft-protocol/436e0f2945d82408cfd1eb4262535c205bcba8d0 mkdirp: 2.1.6 moment: 2.29.4 needle: 2.9.1 diff --git a/prismarine-viewer/package.json b/prismarine-viewer/package.json index 4e49c6fe..91f22889 100644 --- a/prismarine-viewer/package.json +++ b/prismarine-viewer/package.json @@ -38,7 +38,7 @@ "socket.io-client": "^4.0.0", "three-stdlib": "^2.26.11", "three.meshline": "^1.3.0", - "tsx": "^3.13.0", + "tsx": "^4.7.0", "vec3": "^0.1.7" } } From 72ef5423df7aa43b7f69aa89107536a0ce3b5655 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Thu, 4 Jan 2024 05:38:56 +0530 Subject: [PATCH 0024/1316] fix vercel deploy sounds --- scripts/downloadSoundsMap.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/downloadSoundsMap.mjs b/scripts/downloadSoundsMap.mjs index 391d5a1c..066a3df7 100644 --- a/scripts/downloadSoundsMap.mjs +++ b/scripts/downloadSoundsMap.mjs @@ -4,4 +4,7 @@ const url = 'https://github.com/zardoy/prismarine-web-client/raw/sounds-generate const savePath = 'dist/sounds.js' fetch(url).then(res => res.text()).then(data => { fs.writeFileSync(savePath, data, 'utf8') + if (fs.existsSync('.vercel/output/static/')) { + fs.writeFileSync('.vercel/output/static/sounds.js', data, 'utf8') + } }) From ab70e99c0f9f31b9dcd47357466f1e3ce15d32cb Mon Sep 17 00:00:00 2001 From: Vitaly Date: Thu, 4 Jan 2024 05:47:04 +0530 Subject: [PATCH 0025/1316] [skip ci] remove log --- src/soundSystem.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/soundSystem.ts b/src/soundSystem.ts index f1604228..773d2be8 100644 --- a/src/soundSystem.ts +++ b/src/soundSystem.ts @@ -189,7 +189,6 @@ subscribeKey(miscUiState, 'gameLoaded', async () => { registerEvents() - console.log(getVersionedSound('1.13', 'mob/fox/spit1', Object.entries(soundsLegacyMap))) // 1.20+ soundEffectHeard is broken atm // bot._client.on('packet', (data, { name }, buffer) => { // if (name === 'sound_effect') { From c834eceec74a9428cf1080b5b57cfcc9fdd4225e Mon Sep 17 00:00:00 2001 From: Vitaly Date: Thu, 4 Jan 2024 05:55:09 +0530 Subject: [PATCH 0026/1316] fix: block break sound wasnt respecting position --- src/soundSystem.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/soundSystem.ts b/src/soundSystem.ts index 773d2be8..c63c1575 100644 --- a/src/soundSystem.ts +++ b/src/soundSystem.ts @@ -8,6 +8,7 @@ import { options } from './optionsStorage' import { loadOrPlaySound } from './basicSounds' subscribeKey(miscUiState, 'gameLoaded', async () => { + if (!miscUiState.gameLoaded) return const soundsLegacyMap = window.allSoundsVersionedMap as Record const allSoundsMap = window.allSoundsMap as Record> const allSoundsMeta = window.allSoundsMeta as { format: string, baseUrl: string } @@ -182,7 +183,7 @@ subscribeKey(miscUiState, 'gameLoaded', async () => { }) bot.on('diggingCompleted', async () => { if (diggingBlock) { - await playBlockBreak(diggingBlock.name) + await playBlockBreak(diggingBlock.name, diggingBlock.position) } }) } From de66e6cf8495db08769a35952fd4f0be68ad4b41 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Thu, 4 Jan 2024 06:12:27 +0530 Subject: [PATCH 0027/1316] fix: chat messages history was not saving --- src/react/ChatProvider.tsx | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/react/ChatProvider.tsx b/src/react/ChatProvider.tsx index ef900661..d12bac26 100644 --- a/src/react/ChatProvider.tsx +++ b/src/react/ChatProvider.tsx @@ -15,15 +15,17 @@ export default () => { bot.addListener('message', (jsonMsg, position) => { const parts = formatMessage(jsonMsg) - const lastId = messages.at(-1)?.id ?? 0 - const newMessage: Message = { - parts, - id: lastId + 1, - faded: false, - } - setMessages([...messages, newMessage].slice(-messagesLimit)) - fadeMessage(newMessage, true, () => { - setMessages([...messages]) + setMessages(m => { + const lastId = messages.at(-1)?.id ?? 0 + const newMessage: Message = { + parts, + id: lastId + 1, + faded: false, + } + fadeMessage(newMessage, true, () => { + setMessages(m => [...m]) + }) + return [...m, newMessage].slice(-messagesLimit) }) // todo update scrollbottom From 1121b41047ddb23c83d664b7e795edecb5f9907e Mon Sep 17 00:00:00 2001 From: Vitaly Date: Thu, 4 Jan 2024 06:15:11 +0530 Subject: [PATCH 0028/1316] fix: sound was not clipped by 100% and sometimes was louder, add support for a few more world event sounds --- src/soundSystem.ts | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/soundSystem.ts b/src/soundSystem.ts index c63c1575..80315456 100644 --- a/src/soundSystem.ts +++ b/src/soundSystem.ts @@ -53,7 +53,7 @@ subscribeKey(miscUiState, 'gameLoaded', async () => { const isMuted = options.mutedSounds.includes(soundKey) if (position) { if (!isMuted) { - viewer.playSound(position, url, soundVolume * volume * (options.volume / 100)) + viewer.playSound(position, url, soundVolume * Math.max(Math.min(volume, 1), 0) * (options.volume / 100)) } if (getDistance(bot.entity.position, position) < 16) { lastPlayedSounds.lastServerPlayed[soundKey] ??= { count: 0, last: 0 } @@ -176,6 +176,30 @@ subscribeKey(miscUiState, 'gameLoaded', async () => { if (effectId === 2002 || effectId === 2003 || effectId === 2007) { await playHardcodedSound('block.glass.break', position, 1, 1) } + if (effectId === 1004) { + // firework shoot + await playHardcodedSound('entity.firework_rocket.launch', position, 1, 1) + } + if (effectId === 1006 || effectId === 1007 || effectId === 1014) { + // wooden door open/close + await playHardcodedSound('block.wooden_door.open', position, 1, 1) + } + if (effectId === 1002) { + // dispenser shoot + await playHardcodedSound('block.dispenser.dispense', position, 1, 1) + } + if (effectId === 1024) { + // wither shoot + await playHardcodedSound('entity.wither.shoot', position, 1, 1) + } + if (effectId === 1031) { + // anvil land + await playHardcodedSound('block.anvil.land', position, 1, 1) + } + if (effectId === 1010) { + console.log('play record', data) + } + // todo add support for all current world events }) let diggingBlock: Block | null = null customEvents.on('digStart', () => { From e0ab281c6482f895aa726b6de51564fa59b6b450 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Thu, 4 Jan 2024 17:43:47 +0530 Subject: [PATCH 0029/1316] fix(regression): autocomplete wasn't triggered on --- src/react/Chat.stories.tsx | 45 ++++++++++++++++++++++++++----------- src/react/ChatContainer.tsx | 41 ++++++++++++++++++--------------- src/react/ChatProvider.tsx | 7 +++--- 3 files changed, 58 insertions(+), 35 deletions(-) diff --git a/src/react/Chat.stories.tsx b/src/react/Chat.stories.tsx index 2efa4e6c..0dc4f6e0 100644 --- a/src/react/Chat.stories.tsx +++ b/src/react/Chat.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from '@storybook/react' import { useEffect, useState } from 'react' -import Chat, { fadeMessage } from './ChatContainer' +import Chat, { fadeMessage, initialChatOpenValue } from './ChatContainer' import Button from './Button' const meta: Meta = { @@ -9,6 +9,25 @@ const meta: Meta = { render (args) { const [messages, setMessages] = useState(args.messages) const [autoSpam, setAutoSpam] = useState(false) + const [open, setOpen] = useState(args.opened) + + useEffect(() => { + const abortController = new AbortController() + addEventListener('keyup', (e) => { + if (e.code === 'KeyY') { + initialChatOpenValue.value = '/' + setOpen(true) + e.stopImmediatePropagation() + } + if (e.code === 'Escape') { + setOpen(false) + e.stopImmediatePropagation() + } + }, { + signal: abortController.signal, + }) + return () => abortController.abort() + }) useEffect(() => { setMessages(args.messages) @@ -46,7 +65,16 @@ const meta: Meta = { } return
    - + setOpen(false)} fetchCompletionItems={async (triggerType, value) => { + console.log('fetchCompletionItems') + await new Promise(resolve => { + setTimeout(resolve, 700) + }) + let items = ['test', ...Array.from({ length: 50 }).map((_, i) => `minecraft:hello${i}`)] + if (value === '/') items = items.map(item => `/${item}`) + return items + }} /> + @@ -114,15 +142,6 @@ export const Primary: Story = { ], id: 0, }], - opened: false, - async fetchCompletionItems (triggerType, value) { - console.log('fetchCompletionItems') - await new Promise(resolve => { - setTimeout(resolve, 700) - }) - let items = ['test', ...Array.from({ length: 50 }).map((_, i) => `minecraft:hello${i}`)] - if (value === '/') items = items.map(item => `/${item}`) - return items - }, - }, + // opened: false, + } } diff --git a/src/react/ChatContainer.tsx b/src/react/ChatContainer.tsx index 33d72a60..4c0a8038 100644 --- a/src/react/ChatContainer.tsx +++ b/src/react/ChatContainer.tsx @@ -50,7 +50,7 @@ export const fadeMessage = (message: Message, initialTimeout: boolean, requestUp }, initialTimeout ? 5000 : 0) } -const ChatComponent = ({ messages, touch, opacity, fetchCompletionItems, opened, interceptMessage, onClose }: Props) => { +export default ({ messages, touch, opacity, fetchCompletionItems, opened, interceptMessage, onClose }: Props) => { const [sendHistory, _setSendHistory] = useState(JSON.parse(window.sessionStorage.chatHistory || '[]')) const [completePadText, setCompletePadText] = useState('') @@ -80,7 +80,7 @@ const ChatComponent = ({ messages, touch, opacity, fetchCompletionItems, opened, const updateInputValue = (newValue: string) => { chatInput.current.value = newValue - chatInput.current.dispatchEvent(new Event('input')) + onMainInputChange() setTimeout(() => { chatInput.current.setSelectionRange(newValue.length, newValue.length) }, 0) @@ -115,13 +115,18 @@ const ChatComponent = ({ messages, touch, opacity, fetchCompletionItems, opened, updateInputValue(initialChatOpenValue.value) initialChatOpenValue.value = '' chatInput.current.focus() - resetCompletionItems() } if (!opened) { chatMessages.current.scrollTop = chatMessages.current.scrollHeight } }, [opened]) + useMemo(() => { + if (opened) { + resetCompletionItems() + } + }, [opened]) + useEffect(() => { if (!opened || (opened && openedChatWasAtBottom.current)) { openedChatWasAtBottom.current = false @@ -175,6 +180,18 @@ const ChatComponent = ({ messages, touch, opacity, fetchCompletionItems, opened, setCompletionItems(newCompleteItems) } + const onMainInputChange = () => { + const completeValue = getCompleteValue() + setCompletePadText(completeValue === '/' ? '' : completeValue) + if (completeRequestValue.current === completeValue) { + updateFilteredCompleteItems(completionItemsSource) + return + } + if (completeValue === '/') { + void fetchCompletions(true) + } + } + return ( <>
    ) } - -export default ChatComponent diff --git a/src/react/ChatProvider.tsx b/src/react/ChatProvider.tsx index d12bac26..6b3bfb56 100644 --- a/src/react/ChatProvider.tsx +++ b/src/react/ChatProvider.tsx @@ -3,7 +3,7 @@ import { formatMessage } from '../botUtils' import { getBuiltinCommandsList, tryHandleBuiltinCommand } from '../builtinCommands' import { hideCurrentModal } from '../globalState' import { options } from '../optionsStorage' -import ChatComponent, { Message, fadeMessage } from './ChatContainer' +import ChatContainer, { Message, fadeMessage } from './ChatContainer' import { useIsModalActive } from './utils' export default () => { @@ -23,16 +23,15 @@ export default () => { faded: false, } fadeMessage(newMessage, true, () => { + // eslint-disable-next-line max-nested-callbacks setMessages(m => [...m]) }) return [...m, newMessage].slice(-messagesLimit) }) - - // todo update scrollbottom }) }, []) - return { From 28a40c6f6df859f0b9e50e8cf8bab9a92f1ac513 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Fri, 5 Jan 2024 02:18:46 +0530 Subject: [PATCH 0030/1316] fix: now gpu preference can be set to force lower-power gpu in rare scenarios (new setting instead of "use dedicated gpu"). the current setting is just a hint for browser, which can be ignored --- src/index.ts | 2 +- src/optionsGuiScheme.tsx | 7 +++--- src/optionsStorage.ts | 12 +++++++++-- src/react/OptionsGroup.tsx | 6 +++--- src/react/OptionsItems.tsx | 44 ++++++++++++++++++++++++++++++++++---- 5 files changed, 58 insertions(+), 13 deletions(-) diff --git a/src/index.ts b/src/index.ts index cb5d44d4..92c446c2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -97,7 +97,7 @@ watchFov() // Create three.js context, add to page const renderer = new THREE.WebGLRenderer({ - powerPreference: options.highPerformanceGpu ? 'high-performance' : 'default', + powerPreference: options.gpuPreference, }) initWithRenderer(renderer.domElement) window.renderer = renderer diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx index 07242f37..93d63cdb 100644 --- a/src/optionsGuiScheme.tsx +++ b/src/optionsGuiScheme.tsx @@ -11,7 +11,7 @@ import { getResourcePackName, resourcePackState, uninstallTexturePack } from './ import { resetLocalStorageWithoutWorld } from './browserfs' export const guiOptionsScheme: { - [t in OptionsGroupType]: Array<{ [k in keyof AppOptions]?: Partial } & { custom?}> + [t in OptionsGroupType]: Array<{ [K in keyof AppOptions]?: Partial> } & { custom?}> } = { render: [ { @@ -31,10 +31,11 @@ export const guiOptionsScheme: { } }, { - highPerformanceGpu: { + gpuPreference: { // todo reimplement to gpu preference to allow use low-energy instead - text: 'Use Dedicated GPU', + text: 'GPU Preference', // willHaveNoEffect: isIos + values: [['default', 'Auto'], ['high-performance', 'Dedicated'], ['low-power', 'Low Power']] }, }, { diff --git a/src/optionsStorage.ts b/src/optionsStorage.ts index 8d13cb39..41678aa8 100644 --- a/src/optionsStorage.ts +++ b/src/optionsStorage.ts @@ -26,7 +26,7 @@ const defaultOptions = { touchButtonsSize: 40, touchButtonsOpacity: 80, touchButtonsPosition: 12, - highPerformanceGpu: false, + gpuPreference: 'default' as 'default' | 'high-performance' | 'low-power', /** @unstable */ disableAssets: false, /** @unstable */ @@ -52,11 +52,19 @@ const defaultOptions = { mutedSounds: [] as string[] } +const migrateOptions = (options) => { + if (options.highPerformanceGpu) { + options.gpuPreference = 'high-performance' + delete options.highPerformanceGpu + } + return options +} + export type AppOptions = typeof defaultOptions export const options: AppOptions = proxy({ ...defaultOptions, - ...JSON.parse(localStorage.options || '{}') + ...migrateOptions(JSON.parse(localStorage.options || '{}')) }) window.options = window.settings = options diff --git a/src/react/OptionsGroup.tsx b/src/react/OptionsGroup.tsx index ebed9098..bbfac00e 100644 --- a/src/react/OptionsGroup.tsx +++ b/src/react/OptionsGroup.tsx @@ -3,8 +3,8 @@ import { options } from '../optionsStorage' import { OptionsGroupType, guiOptionsScheme } from '../optionsGuiScheme' import OptionsItems, { OptionMeta } from './OptionsItems' -const optionValueToType = (optionValue: any) => { - if (typeof optionValue === 'boolean') return 'toggle' +const optionValueToType = (optionValue: any, item: OptionMeta) => { + if (typeof optionValue === 'boolean' || item.values) return 'toggle' if (typeof optionValue === 'number') return 'slider' if (typeof optionValue === 'string') return 'element' } @@ -14,7 +14,7 @@ const finalItemsScheme: Record = Ob return Object.entries(optionsObj).map(([optionKey, metaMerge]) => { const optionValue = options[optionKey] - const type = optionValueToType(optionValue) + const type = optionValueToType(optionValue, metaMerge) const meta: OptionMeta = { id: optionKey === 'custom' ? undefined : optionKey, type, diff --git a/src/react/OptionsItems.tsx b/src/react/OptionsItems.tsx index 92f1de5e..5c0941e6 100644 --- a/src/react/OptionsItems.tsx +++ b/src/react/OptionsItems.tsx @@ -7,15 +7,16 @@ import Button from './Button' import Slider from './Slider' import Screen from './Screen' -type GeneralItem = { +type GeneralItem = { id?: string text?: string, disabledReason?: string, tooltip?: string willHaveNoEffect?: boolean + values?: Array } -export type OptionMeta = GeneralItem & ({ +export type OptionMeta = GeneralItem & ({ type: 'toggle', } | { type: 'slider' @@ -32,10 +33,45 @@ export type OptionMeta = GeneralItem & ({ export const OptionButton = ({ item }: { item: Extract }) => { const optionValue = useSnapshot(options)[item.id!] + const valuesTitlesMap = useMemo(() => { + if (!item.values) { + return { + true: 'ON', + false: 'OFF', + } + } + return Object.fromEntries(item.values.map((value) => { + if (typeof value === 'string') { + return [value, titleCase(noCase(value))] + } else { + return [value[0], value[1]] + } + })) + }, [item.values]) + return - + }, } diff --git a/src/react/MessageFormatted.tsx b/src/react/MessageFormatted.tsx index 04a112cc..f4e07297 100644 --- a/src/react/MessageFormatted.tsx +++ b/src/react/MessageFormatted.tsx @@ -39,13 +39,14 @@ export function getColorShadow (hex, dim = 0.25) { return `#${f(r)}${f(g)}${f(b)}` } -function parseInlineStyle (style: string): Record { - const template = document.createElement('template') - template.setAttribute('style', style) - return Object.fromEntries(Object.entries(template.style) - .filter(([key]) => !/^\d+$/.test(key)) - .filter(([, value]) => Boolean(value)) - .map(([key, value]) => [key, value])) +export function parseInlineStyle (style: string): Record { + const obj: Record = {} + for (const rule of style.split(';')) { + const [prop, value] = rule.split(':') + const cssInJsProp = prop.trim().replaceAll(/-./g, (x) => x.toUpperCase()[1]) + obj[cssInJsProp] = value.trim() + } + return obj } export const messageFormatStylesMap = { From 9c4c439142f11c8862996b06386c46e25065039e Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Fri, 5 Jan 2024 23:41:25 +0530 Subject: [PATCH 0036/1316] feat: now you can control opacity of the chat (and in opened state too!) feat: now camera sens Y defaults to the sens X value (can be set to mirror X value by moving slider to the left) fix(regression): chat mobile styles were incorrect, fix chat overflow when settings are opened --- src/basicSounds.ts | 7 ++ src/index.ts | 2 +- src/optionsGuiScheme.tsx | 14 +++- src/optionsStorage.ts | 5 +- src/react/ChatContainer.css | 140 +++++++++++++++++-------------- src/react/ChatContainer.tsx | 17 ++-- src/react/ChatProvider.tsx | 3 +- src/react/TouchControls.tsx | 76 +++++++++++++++++ src/{reactUi.jsx => reactUi.tsx} | 77 +---------------- src/screens.css | 1 + src/soundSystem.ts | 6 +- 11 files changed, 194 insertions(+), 154 deletions(-) create mode 100644 src/react/TouchControls.tsx rename src/{reactUi.jsx => reactUi.tsx} (52%) diff --git a/src/basicSounds.ts b/src/basicSounds.ts index 40b6963e..a16193fe 100644 --- a/src/basicSounds.ts +++ b/src/basicSounds.ts @@ -1,4 +1,5 @@ import { options } from './optionsStorage' +import { isCypress } from './standaloneUtils' let audioContext: AudioContext const sounds: Record = {} @@ -10,6 +11,12 @@ export async function loadSound (path: string) { if (loadingSounds.includes(path)) return true loadingSounds.push(path) const res = await window.fetch(path) + if (!res.ok) { + const error = `Failed to load sound ${path}` + if (isCypress()) throw new Error(error) + else console.warn(error) + return + } const data = await res.arrayBuffer() sounds[path] = data diff --git a/src/index.ts b/src/index.ts index b5511619..7d6f8f62 100644 --- a/src/index.ts +++ b/src/index.ts @@ -196,7 +196,7 @@ function onCameraMove (e) { if (now - lastMouseMove < 4) return lastMouseMove = now let { mouseSensX, mouseSensY } = options - if (mouseSensY === true) mouseSensY = mouseSensX + if (mouseSensY === -1) mouseSensY = mouseSensX mouseMovePostHandle({ x: e.movementX * mouseSensX * 0.0001, y: e.movementY * mouseSensY * 0.0001 diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx index 93d63cdb..a6d606c9 100644 --- a/src/optionsGuiScheme.tsx +++ b/src/optionsGuiScheme.tsx @@ -32,9 +32,8 @@ export const guiOptionsScheme: { }, { gpuPreference: { - // todo reimplement to gpu preference to allow use low-energy instead text: 'GPU Preference', - // willHaveNoEffect: isIos + tooltip: 'You will need to reload the page for this to take effect.', values: [['default', 'Auto'], ['high-performance', 'Dedicated'], ['low-power', 'Low Power']] }, }, @@ -146,13 +145,22 @@ export const guiOptionsScheme: { max: 180, unit: 'px', }, + chatOpacity: { + }, + chatOpacityOpened: { + }, } ], controls: [ { // keybindings mouseSensX: {}, - mouseSensY: {}, + mouseSensY: { + min: -1, + valueText (value) { + return value === -1 ? 'Same as X' : `${value}` + }, + }, mouseRawInput: { tooltip: 'Wether to disable any mouse acceleration (MC does it by default). Most probably it is still supported only by Chrome.', // eslint-disable-next-line no-extra-boolean-cast diff --git a/src/optionsStorage.ts b/src/optionsStorage.ts index 41678aa8..6796eb83 100644 --- a/src/optionsStorage.ts +++ b/src/optionsStorage.ts @@ -13,11 +13,14 @@ const defaultOptions = { autoExitFullscreen: false, localUsername: 'wanderer', mouseSensX: 50, - mouseSensY: 50 as number | true, + mouseSensY: -1, // mouseInvertX: false, chatWidth: 320, chatHeight: 180, chatScale: 100, + chatOpacity: 100, + chatOpacityOpened: 100, + messagesLimit: 200, volume: 50, // fov: 70, fov: 75, diff --git a/src/react/ChatContainer.css b/src/react/ChatContainer.css index c32dcd52..f8f7d0fb 100644 --- a/src/react/ChatContainer.css +++ b/src/react/ChatContainer.css @@ -1,33 +1,36 @@ -div.chat-wrapper { /* increase specificity */ - position: fixed; - z-index: 10; - padding-left: calc(env(safe-area-inset-left) / 2); - padding-right: calc(env(safe-area-inset-right, 4px) / 2); +div.chat-wrapper { + /* increase specificity */ + position: fixed; + /* z-index: 10; */ + padding-left: calc(env(safe-area-inset-left) / 2); + padding-right: calc(env(safe-area-inset-right, 4px) / 2); } .chat-messages-wrapper { - bottom: 40px; - padding: 4px; - padding-left: 0; - max-height: var(--chatHeight); - width: var(--chatWidth); - transform-origin: bottom left; - transform: scale(var(--chatScale)); - pointer-events: none; + bottom: 40px; + padding: 4px; + padding-left: 0; + max-height: var(--chatHeight); + width: var(--chatWidth); + transform-origin: bottom left; + transform: scale(var(--chatScale)); + pointer-events: none; } .chat-input-wrapper { - bottom: 1px; - width: calc(100% - 3px); - position: fixed; - left: 1px; - box-sizing: border-box; - background-color: rgba(0, 0, 0, 0); + bottom: 1px; + width: calc(100% - 3px); + position: fixed; + left: 1px; + box-sizing: border-box; + background-color: rgba(0, 0, 0, 0); } + .chat-input { box-sizing: border-box; width: 100%; } + .chat-completions { position: absolute; /* position this bottom on top of parent */ @@ -36,19 +39,24 @@ div.chat-wrapper { /* increase specificity */ transform: translateY(-100%); /* width: 150px; */ display: flex; - padding: 0 2px; /* input padding */ + padding: 0 2px; + /* input padding */ width: 100%; } + .input-mobile .chat-completions { transform: none; - top: 15px; /* input height */ + top: 15px; + /* input height */ } + .chat-completions-pad-text { pointer-events: none; white-space: pre; opacity: 0; overflow: hidden; } + .chat-completions-items { background-color: rgba(0, 0, 0, 0.5); display: flex; @@ -61,28 +69,34 @@ div.chat-wrapper { /* increase specificity */ /* hide ugly scrollbars in firefox */ scrollbar-width: none; } + /* unsupported by firefox */ ::-webkit-scrollbar { - width: 5px; - height: 5px; - background-color: rgb(24, 24, 24); + width: 5px; + height: 5px; + background-color: rgb(24, 24, 24); } + ::-webkit-scrollbar-thumb { - background-color: rgb(50, 50, 50); + background-color: rgb(50, 50, 50); } -.chat-completions-items > div { + +.chat-completions-items>div { cursor: pointer; } -.chat-completions-items > div:hover { + +.chat-completions-items>div:hover { text-shadow: 0px 0px 6px white; } + .input-mobile .chat-completions-items { justify-content: flex-start; } .input-mobile { - top: 1px; + top: 15px; } + .input-mobile #chatinput { height: 20px; } @@ -91,37 +105,41 @@ div.chat-wrapper { /* increase specificity */ top: 40px; } -.chat, .chat-input { - color: white; - font-size: 10px; - margin: 0px; - line-height: 100%; - text-shadow: 1px 1px 0px #3f3f3f; - font-family: mojangles, minecraft, monospace; - max-height: var(--chatHeight); +.chat, +.chat-input { + color: white; + font-size: 10px; + margin: 0px; + line-height: 100%; + text-shadow: 1px 1px 0px #3f3f3f; + font-family: mojangles, minecraft, monospace; + max-height: var(--chatHeight); } + .chat { pointer-events: none; overflow: hidden; width: 100%; scrollbar-width: thin; } + .chat.opened { - pointer-events: auto; - overflow-y: auto; + pointer-events: auto; + overflow-y: auto; } -input[type=text], #chatinput { - background-color: rgba(0, 0, 0, 0.5); - border: 1px solid rgba(0, 0, 0, 0); - outline: none; - pointer-events: auto; - /* styles reset */ - padding-top: 1px; - padding-bottom: 1px; - padding-left: 2px; - padding-right: 2px; - height: 15px; +input[type=text], +#chatinput { + background-color: rgba(0, 0, 0, 0.5); + border: 1px solid rgba(0, 0, 0, 0); + outline: none; + pointer-events: auto; + /* styles reset */ + padding-top: 1px; + padding-bottom: 1px; + padding-left: 2px; + padding-right: 2px; + height: 15px; } .chat-mobile-hidden { @@ -132,6 +150,7 @@ input[type=text], #chatinput { opacity: 0; pointer-events: none; } + .chat-mobile-hidden:nth-last-child(1) { height: 8px; } @@ -141,29 +160,28 @@ input[type=text], #chatinput { } .chat-message { - padding-left: 4px; - background-color: rgba(0, 0, 0, 0.5); - list-style: none; - word-break: break-all; + padding-left: 4px; + background-color: rgba(0, 0, 0, 0.5); + list-style: none; + word-break: break-all; } .chat-message-fadeout { - opacity: 1; - transition: all 3s; + opacity: 1; + transition: all 3s; } .chat-message-fade { - opacity: 0; + opacity: 0; } .chat-message-faded { - transition: none !important; + transition: none !important; } .chat.opened .chat-message { - opacity: 1 !important; - transition: none !important; + opacity: 1 !important; + transition: none !important; } -.chat-message-part { -} +.chat-message-part {} diff --git a/src/react/ChatContainer.tsx b/src/react/ChatContainer.tsx index f5a82f3f..852abd7d 100644 --- a/src/react/ChatContainer.tsx +++ b/src/react/ChatContainer.tsx @@ -27,7 +27,6 @@ const MessageLine = ({ message }) => { type Props = { messages: Message[] - touch?: boolean opacity?: number opened?: boolean onClose?: () => void @@ -51,7 +50,7 @@ export const fadeMessage = (message: Message, initialTimeout: boolean, requestUp }, initialTimeout ? 5000 : 0) } -export default ({ messages, touch, opacity, fetchCompletionItems, opened, interceptMessage, onClose }: Props) => { +export default ({ messages, opacity = 1, fetchCompletionItems, opened, interceptMessage, onClose }: Props) => { const usingTouch = useUsingTouch() const [sendHistory, _setSendHistory] = useState(JSON.parse(window.sessionStorage.chatHistory || '[]')) @@ -62,7 +61,7 @@ export default ({ messages, touch, opacity, fetchCompletionItems, opened, interc const [completionItems, setCompletionItems] = useState([] as string[]) const chatInput = useRef(null!) - const chatMessages = useRef(null!) + const chatMessages = useRef(null) const openedChatWasAtBottom = useRef(false) const chatHistoryPos = useRef(0) const inputCurrentlyEnteredValue = useRef('') @@ -121,7 +120,7 @@ export default ({ messages, touch, opacity, fetchCompletionItems, opened, interc chatInput.current.focus() } } - if (!opened) { + if (!opened && chatMessages.current) { chatMessages.current.scrollTop = chatMessages.current.scrollHeight } }, [opened]) @@ -133,7 +132,7 @@ export default ({ messages, touch, opacity, fetchCompletionItems, opened, interc }, [opened]) useEffect(() => { - if (!opened || (opened && openedChatWasAtBottom.current)) { + if ((!opened || (opened && openedChatWasAtBottom.current)) && chatMessages.current) { openedChatWasAtBottom.current = false // stay at bottom on messages changes chatMessages.current.scrollTop = chatMessages.current.scrollHeight @@ -199,15 +198,15 @@ export default ({ messages, touch, opacity, fetchCompletionItems, opened, interc return ( <> -