diff --git a/patches/minecraft-protocol@1.53.0.patch b/patches/minecraft-protocol@1.53.0.patch deleted file mode 100644 index 243e0bd7d..000000000 --- a/patches/minecraft-protocol@1.53.0.patch +++ /dev/null @@ -1,188 +0,0 @@ -diff --git a/src/client/autoVersion.js b/src/client/autoVersion.js -index c437ecf3a0e4ab5758a48538c714b7e9651bb5da..d9c9895ae8614550aa09ad60a396ac32ffdf1287 100644 ---- a/src/client/autoVersion.js -+++ b/src/client/autoVersion.js -@@ -9,7 +9,7 @@ module.exports = function (client, options) { - client.wait_connect = true // don't let src/client/setProtocol proceed on socket 'connect' until 'connect_allowed' - debug('pinging', options.host) - // TODO: use 0xfe ping instead for better compatibility/performance? https://github.com/deathcap/node-minecraft-ping -- ping(options, function (err, response) { -+ ping(options, async function (err, response) { - if (err) { return client.emit('error', err) } - debug('ping response', response) - // TODO: could also use ping pre-connect to save description, type, max players, etc. -@@ -40,6 +40,7 @@ module.exports = function (client, options) { - - // Reinitialize client object with new version TODO: move out of its constructor? - client.version = minecraftVersion -+ await options.versionSelectedHook?.(client) - client.state = states.HANDSHAKING - - // Let other plugins such as Forge/FML (modinfo) respond to the ping response -diff --git a/src/client/chat.js b/src/client/chat.js -index f14269bea055d4329cd729271e7406ec4b344de7..00f5482eb6e3c911381ca9a728b1b4aae0d1d337 100644 ---- a/src/client/chat.js -+++ b/src/client/chat.js -@@ -111,7 +111,7 @@ module.exports = function (client, options) { - for (const player of packet.data) { - if (!player.chatSession) continue - client._players[player.UUID] = { -- publicKey: crypto.createPublicKey({ key: player.chatSession.publicKey.keyBytes, format: 'der', type: 'spki' }), -+ // publicKey: crypto.createPublicKey({ key: player.chatSession.publicKey.keyBytes, format: 'der', type: 'spki' }), - publicKeyDER: player.chatSession.publicKey.keyBytes, - sessionUuid: player.chatSession.uuid - } -@@ -127,7 +127,7 @@ module.exports = function (client, options) { - for (const player of packet.data) { - if (player.crypto) { - client._players[player.UUID] = { -- publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }), -+ // publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }), - publicKeyDER: player.crypto.publicKey, - signature: player.crypto.signature, - displayName: player.displayName || player.name -@@ -198,7 +198,7 @@ module.exports = function (client, options) { - if (mcData.supportFeature('useChatSessions')) { - const tsDelta = BigInt(Date.now()) - packet.timestamp - const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0 -- const verified = !packet.unsignedChatContent && updateAndValidateSession(packet.senderUuid, packet.plainMessage, packet.signature, packet.index, packet.previousMessages, packet.salt, packet.timestamp) && !expired -+ const verified = false && !packet.unsignedChatContent && updateAndValidateSession(packet.senderUuid, packet.plainMessage, packet.signature, packet.index, packet.previousMessages, packet.salt, packet.timestamp) && !expired - if (verified) client._signatureCache.push(packet.signature) - client.emit('playerChat', { - plainMessage: packet.plainMessage, -@@ -363,7 +363,7 @@ module.exports = function (client, options) { - } - } - -- client._signedChat = (message, options = {}) => { -+ client._signedChat = async (message, options = {}) => { - options.timestamp = options.timestamp || BigInt(Date.now()) - options.salt = options.salt || 1n - -@@ -405,7 +405,7 @@ module.exports = function (client, options) { - message, - timestamp: options.timestamp, - salt: options.salt, -- signature: (client.profileKeys && client._session) ? client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined, -+ signature: (client.profileKeys && client._session) ? await client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined, - offset: client._lastSeenMessages.pending, - acknowledged - }) -@@ -419,7 +419,7 @@ module.exports = function (client, options) { - message, - timestamp: options.timestamp, - salt: options.salt, -- signature: client.profileKeys ? client.signMessage(message, options.timestamp, options.salt, options.preview) : Buffer.alloc(0), -+ signature: client.profileKeys ? await client.signMessage(message, options.timestamp, options.salt, options.preview) : Buffer.alloc(0), - signedPreview: options.didPreview, - previousMessages: client._lastSeenMessages.map((e) => ({ - messageSender: e.sender, -diff --git a/src/client/encrypt.js b/src/client/encrypt.js -index b9d21bab9faccd5dbf1975fc423fc55c73e906c5..99ffd76527b410e3a393181beb260108f4c63536 100644 ---- a/src/client/encrypt.js -+++ b/src/client/encrypt.js -@@ -25,7 +25,11 @@ module.exports = function (client, options) { - if (packet.serverId !== '-') { - debug('This server appears to be an online server and you are providing no password, the authentication will probably fail') - } -- sendEncryptionKeyResponse() -+ client.end('This server appears to be an online server and you are providing no authentication. Try authenticating first.') -+ // sendEncryptionKeyResponse() -+ // client.once('set_compression', () => { -+ // clearTimeout(loginTimeout) -+ // }) - } - - function onJoinServerResponse (err) { -diff --git a/src/client.js b/src/client.js -index 74749698f8cee05b5dc749c271544f78d06645b0..e77e0a3f41c1ee780c3abbd54b0801d248c2a07c 100644 ---- a/src/client.js -+++ b/src/client.js -@@ -89,10 +89,12 @@ class Client extends EventEmitter { - parsed.metadata.name = parsed.data.name - parsed.data = parsed.data.params - parsed.metadata.state = state -- debug('read packet ' + state + '.' + parsed.metadata.name) -- if (debug.enabled) { -- const s = JSON.stringify(parsed.data, null, 2) -- debug(s && s.length > 10000 ? parsed.data : s) -+ if (!globalThis.excludeCommunicationDebugEvents?.includes(parsed.metadata.name)) { -+ debug('read packet ' + state + '.' + parsed.metadata.name) -+ if (debug.enabled) { -+ const s = JSON.stringify(parsed.data, null, 2) -+ debug(s && s.length > 10000 ? parsed.data : s) -+ } - } - if (this._hasBundlePacket && parsed.metadata.name === 'bundle_delimiter') { - if (this._mcBundle.length) { // End bundle -@@ -110,7 +112,13 @@ class Client extends EventEmitter { - this._hasBundlePacket = false - } - } else { -- emitPacket(parsed) -+ try { -+ emitPacket(parsed) -+ } catch (err) { -+ console.log('Client incorrectly handled packet ' + parsed.metadata.name) -+ console.error(err) -+ // todo investigate why it doesn't close the stream even if unhandled there -+ } - } - }) - } -@@ -168,7 +176,10 @@ class Client extends EventEmitter { - } - - const onFatalError = (err) => { -- this.emit('error', err) -+ // todo find out what is trying to write after client disconnect -+ if(err.code !== 'ECONNABORTED') { -+ this.emit('error', err) -+ } - endSocket() - } - -@@ -197,6 +208,8 @@ class Client extends EventEmitter { - serializer -> framer -> socket -> splitter -> deserializer */ - if (this.serializer) { - this.serializer.end() -+ this.socket?.end() -+ this.socket?.emit('end') - } else { - if (this.socket) this.socket.end() - } -@@ -238,8 +251,11 @@ class Client extends EventEmitter { - - write (name, params) { - if (!this.serializer.writable) { return } -- debug('writing packet ' + this.state + '.' + name) -- debug(params) -+ if (!globalThis.excludeCommunicationDebugEvents?.includes(name)) { -+ debug(`[${this.state}] from ${this.isServer ? 'server' : 'client'}: ` + name) -+ debug(params) -+ } -+ this.emit('writePacket', name, params) - this.serializer.write({ name, params }) - } - -diff --git a/src/index.d.ts b/src/index.d.ts -index e61d5403bab46251d35b22a2ea30eb09b2746a26..84f597427893671eeac231b11e6e42aa815601df 100644 ---- a/src/index.d.ts -+++ b/src/index.d.ts -@@ -135,6 +135,7 @@ declare module 'minecraft-protocol' { - sessionServer?: string - keepAlive?: boolean - closeTimeout?: number -+ closeTimeout?: number - noPongTimeout?: number - checkTimeoutInterval?: number - version?: string -@@ -155,6 +156,8 @@ declare module 'minecraft-protocol' { - disableChatSigning?: boolean - /** Pass custom client implementation if needed. */ - Client?: Client -+ /** Can be used to prepare mc data on autoVersion (client.version has selected version) */ -+ versionSelectedHook?: (client: Client) => Promise | void - } - - export class Server extends EventEmitter { diff --git a/patches/minecraft-protocol@1.54.0.patch b/patches/minecraft-protocol@1.54.0.patch index 32371450f..29111f695 100644 --- a/patches/minecraft-protocol@1.54.0.patch +++ b/patches/minecraft-protocol@1.54.0.patch @@ -1,24 +1,3 @@ -diff --git a/src/client/autoVersion.js b/src/client/autoVersion.js -index 3fe1552672e4c0dd1b14b3b56950c3d7eaf3537b..6eb615e5827279c328d5547b5911626693252da4 100644 ---- a/src/client/autoVersion.js -+++ b/src/client/autoVersion.js -@@ -9,7 +9,7 @@ module.exports = function (client, options) { - client.wait_connect = true // don't let src/client/setProtocol proceed on socket 'connect' until 'connect_allowed' - debug('pinging', options.host) - // TODO: use 0xfe ping instead for better compatibility/performance? https://github.com/deathcap/node-minecraft-ping -- ping(options, function (err, response) { -+ ping(options, async function (err, response) { - if (err) { return client.emit('error', err) } - debug('ping response', response) - // TODO: could also use ping pre-connect to save description, type, max players, etc. -@@ -40,6 +40,7 @@ module.exports = function (client, options) { - - // Reinitialize client object with new version TODO: move out of its constructor? - client.version = minecraftVersion -+ await options.versionSelectedHook?.(client) - client.state = states.HANDSHAKING - - // Let other plugins such as Forge/FML (modinfo) respond to the ping response diff --git a/src/client/chat.js b/src/client/chat.js index f14269bea055d4329cd729271e7406ec4b344de7..00f5482eb6e3c911381ca9a728b1b4aae0d1d337 100644 --- a/src/client/chat.js @@ -165,24 +144,3 @@ index 74749698f8cee05b5dc749c271544f78d06645b0..e77e0a3f41c1ee780c3abbd54b0801d2 this.serializer.write({ name, params }) } -diff --git a/src/index.d.ts b/src/index.d.ts -index e61d5403bab46251d35b22a2ea30eb09b2746a26..84f597427893671eeac231b11e6e42aa815601df 100644 ---- a/src/index.d.ts -+++ b/src/index.d.ts -@@ -135,6 +135,7 @@ declare module 'minecraft-protocol' { - sessionServer?: string - keepAlive?: boolean - closeTimeout?: number -+ closeTimeout?: number - noPongTimeout?: number - checkTimeoutInterval?: number - version?: string -@@ -155,6 +156,8 @@ declare module 'minecraft-protocol' { - disableChatSigning?: boolean - /** Pass custom client implementation if needed. */ - Client?: Client -+ /** Can be used to prepare mc data on autoVersion (client.version has selected version) */ -+ versionSelectedHook?: (client: Client) => Promise | void - } - - export class Server extends EventEmitter { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f278cf2d8..b6d752117 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,7 +21,7 @@ overrides: patchedDependencies: minecraft-protocol@1.54.0: - hash: 3wm2z233n46lqi64rbxem4nyv4 + hash: dkeyukcqlupmk563gwxsmjr3yu path: patches/minecraft-protocol@1.54.0.patch mineflayer-item-map-downloader@1.2.0: hash: bck55yjvd4wrgz46x7o4vfur5q @@ -138,7 +138,7 @@ importers: version: 3.83.1 minecraft-protocol: specifier: github:PrismarineJS/node-minecraft-protocol#master - version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/e9eb551ba30ec2e742c49e6927be6402b413bb76(patch_hash=3wm2z233n46lqi64rbxem4nyv4)(encoding@0.1.13) + version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/e9eb551ba30ec2e742c49e6927be6402b413bb76(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13) mineflayer-item-map-downloader: specifier: github:zardoy/mineflayer-item-map-downloader version: https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=bck55yjvd4wrgz46x7o4vfur5q)(encoding@0.1.13) @@ -12780,7 +12780,7 @@ snapshots: flatmap: 0.0.3 long: 5.2.3 minecraft-data: 3.83.1 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/e9eb551ba30ec2e742c49e6927be6402b413bb76(patch_hash=3wm2z233n46lqi64rbxem4nyv4)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/e9eb551ba30ec2e742c49e6927be6402b413bb76(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 @@ -16937,7 +16937,7 @@ snapshots: - '@types/react' - react - minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/e9eb551ba30ec2e742c49e6927be6402b413bb76(patch_hash=3wm2z233n46lqi64rbxem4nyv4)(encoding@0.1.13): + minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/e9eb551ba30ec2e742c49e6927be6402b413bb76(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13): dependencies: '@types/node-rsa': 1.1.4 '@types/readable-stream': 4.0.12 @@ -17007,7 +17007,7 @@ snapshots: mineflayer@4.25.0(encoding@0.1.13): dependencies: minecraft-data: 3.83.1 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/e9eb551ba30ec2e742c49e6927be6402b413bb76(patch_hash=3wm2z233n46lqi64rbxem4nyv4)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/e9eb551ba30ec2e742c49e6927be6402b413bb76(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13) prismarine-biome: 1.3.0(minecraft-data@3.83.1)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0) prismarine-chat: 1.10.1 @@ -17030,7 +17030,7 @@ snapshots: mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/54f8c2282d822ad02967a197bda36302a4e7b4a5(encoding@0.1.13): dependencies: minecraft-data: 3.83.1 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/e9eb551ba30ec2e742c49e6927be6402b413bb76(patch_hash=3wm2z233n46lqi64rbxem4nyv4)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/e9eb551ba30ec2e742c49e6927be6402b413bb76(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13) prismarine-biome: 1.3.0(minecraft-data@3.83.1)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0) prismarine-chat: 1.10.1 diff --git a/scripts/makeOptimizedMcData.mjs b/scripts/makeOptimizedMcData.mjs index 74477d7f4..ebc97b599 100644 --- a/scripts/makeOptimizedMcData.mjs +++ b/scripts/makeOptimizedMcData.mjs @@ -242,4 +242,4 @@ const initialMcData = { } } -fs.writeFileSync('./generated/minecraft-initial-data.json', JSON.stringify(initialMcData), 'utf8') +// fs.writeFileSync('./generated/minecraft-initial-data.json', JSON.stringify(initialMcData), 'utf8') diff --git a/src/appParams.ts b/src/appParams.ts index e862e301c..0b6bd6d84 100644 --- a/src/appParams.ts +++ b/src/appParams.ts @@ -1,4 +1,4 @@ -const qsParams = new URLSearchParams(window.location.search) +const qsParams = new URLSearchParams(window.location?.search ?? '') export type AppQsParams = { // AddServerOrConnect.tsx params @@ -37,7 +37,7 @@ export type AppQsParams = { command?: string // Misc params suggest_save?: string - scene?: string + noPacketsValidation?: string } export type AppQsParamsArray = { diff --git a/src/connect.ts b/src/connect.ts index e45769f75..7dd947959 100644 --- a/src/connect.ts +++ b/src/connect.ts @@ -1,9 +1,11 @@ -import { versionsByMinecraftVersion } from 'minecraft-data' -import minecraftInitialDataJson from '../generated/minecraft-initial-data.json' +// import { versionsByMinecraftVersion } from 'minecraft-data' +// import minecraftInitialDataJson from '../generated/minecraft-initial-data.json' import { AuthenticatedAccount } from './react/ServersListProvider' import { setLoadingScreenStatus } from './utils' import { downloadSoundsIfNeeded } from './sounds/botSoundSystem' import { miscUiState } from './globalState' +import { options } from './optionsStorage' +import supportedVersions from './supportedVersions.mjs' export type ConnectOptions = { server?: string @@ -24,21 +26,39 @@ export type ConnectOptions = { viewerWsConnect?: string } -export const downloadNeededDataOnConnect = async (version: string) => { - // todo expose cache - const initialDataVersion = Object.keys(minecraftInitialDataJson)[0]! - if (version === initialDataVersion) { - // ignore cache hit - versionsByMinecraftVersion.pc[initialDataVersion]!.dataVersion!++ +export const getVersionAutoSelect = (autoVersionSelect = options.serversAutoVersionSelect) => { + if (autoVersionSelect === 'auto') { + return '1.20.4' } - setLoadingScreenStatus(`Loading data for ${version}`) - if (!document.fonts.check('1em mojangles')) { + if (autoVersionSelect === 'latest') { + return supportedVersions.at(-1)! + } + return autoVersionSelect +} + +export const downloadMcDataOnConnect = async (version: string) => { + // setLoadingScreenStatus(`Loading data for ${version}`) + // // todo expose cache + // // const initialDataVersion = Object.keys(minecraftInitialDataJson)[0]! + // // if (version === initialDataVersion) { + // // // ignore cache hit + // // versionsByMinecraftVersion.pc[initialDataVersion]!.dataVersion!++ + // // } + + // await window._MC_DATA_RESOLVER.promise // ensure data is loaded + // miscUiState.loadedDataVersion = version +} + +const loadFonts = async () => { + const FONT_FAMILY = 'mojangles' + if (!document.fonts.check(`1em ${FONT_FAMILY}`)) { // todo instead re-render signs on load - await document.fonts.load('1em mojangles').catch(() => { + await document.fonts.load(`1em ${FONT_FAMILY}`).catch(() => { console.error('Failed to load font, signs wont be rendered correctly') }) } - await window._MC_DATA_RESOLVER.promise // ensure data is loaded - await downloadSoundsIfNeeded() - miscUiState.loadedDataVersion = version +} + +export const downloadOtherGameData = async () => { + await Promise.all([loadFonts(), downloadSoundsIfNeeded()]) } diff --git a/src/importsWorkaround.js b/src/importsWorkaround.js index 21bc4585d..231654caa 100644 --- a/src/importsWorkaround.js +++ b/src/importsWorkaround.js @@ -1,4 +1,6 @@ // workaround for mineflayer +globalThis.window ??= globalThis +globalThis.localStorage ??= {} process.versions.node = '18.0.0' if (!navigator.getGamepads) { diff --git a/src/index.ts b/src/index.ts index 36ced1996..4a788b372 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,8 +5,10 @@ import './globals' import './devtools' import './entities' import './globalDomListeners' +import './mineflayer/mc-protocol' import './mineflayer/maps' import './mineflayer/cameraShake' +import './shims/patchShims' import { onGameLoad } from './inventoryWindows' import initCollisionShapes from './getCollisionInteractionShapes' import protocolMicrosoftAuth from 'minecraft-protocol/src/client/microsoftAuth' @@ -86,7 +88,7 @@ import { saveToBrowserMemory } from './react/PauseScreen' import { ViewerWrapper } from 'prismarine-viewer/viewer/lib/viewerWrapper' import './devReload' import './water' -import { ConnectOptions, downloadNeededDataOnConnect } from './connect' +import { ConnectOptions, downloadMcDataOnConnect, getVersionAutoSelect, downloadOtherGameData } from './connect' import { ref, subscribe } from 'valtio' import { signInMessageState } from './react/SignInMessageProvider' import { updateAuthenticatedAccountData, updateLoadedServerData } from './react/ServersListProvider' @@ -99,6 +101,7 @@ import { parseFormattedMessagePacket } from './botUtils' import { getViewerVersionData, getWsProtocolStream } from './viewerConnector' import { appQueryParams, appQueryParamsArray } from './appParams' import { updateCursor } from './cameraRotationControls' +import { pingServerVersion } from './mineflayer/minecraft-protocol-extra' window.debug = debug window.THREE = THREE @@ -270,10 +273,14 @@ export async function connect (connectOptions: ConnectOptions) { const proxy = cleanConnectIp(connectOptions.proxy, undefined) let { username } = connectOptions - console.log(`connecting to ${server.host}:${server.port} with ${username}`) + if (connectOptions.server) { + console.log(`connecting to ${server.host}:${server.port}`) + } + console.log('using player username', username) hideCurrentScreens() - setLoadingScreenStatus('Logging in') + const loggingInMsg = connectOptions.server ? 'Connecting to server' : 'Logging in' + setLoadingScreenStatus(loggingInMsg) let ended = false let bot!: typeof __type_bot @@ -357,14 +364,19 @@ export async function connect (connectOptions: ConnectOptions) { try { const serverOptions = defaultsDeep({}, connectOptions.serverOverrides ?? {}, options.localServerOptions, defaultServerOptions) Object.assign(serverOptions, connectOptions.serverOverridesFlat ?? {}) - window._LOAD_MC_DATA() // start loading data (if not loaded yet) + setLoadingScreenStatus('Downloading minecraft data') + await Promise.all([ + window._LOAD_MC_DATA(), // download mc data before we can use minecraft-data at all + downloadOtherGameData() + ]) + setLoadingScreenStatus(loggingInMsg) const downloadMcData = async (version: string) => { if (connectOptions.authenticatedAccount && (versionToNumber(version) < versionToNumber('1.19.4') || versionToNumber(version) >= versionToNumber('1.21'))) { // todo support it (just need to fix .export crash) throw new Error('Microsoft authentication is only supported on 1.19.4 - 1.20.6 (at least for now)') } - await downloadNeededDataOnConnect(version) + await downloadMcDataOnConnect(version) try { await resourcepackReload(version) } catch (err) { @@ -421,8 +433,14 @@ export async function connect (connectOptions: ConnectOptions) { initialLoadingText = 'Local server is still starting' } else if (p2pMultiplayer) { initialLoadingText = 'Connecting to peer' + } else if (connectOptions.server) { + const versionAutoSelect = getVersionAutoSelect() + setLoadingScreenStatus(`Fetching server version. Preffered: ${versionAutoSelect}`) + const autoVersionSelect = await pingServerVersion(server.host!, server.port ? Number(server.port) : undefined, versionAutoSelect) + initialLoadingText = `Connecting to server ${server.host} with version ${autoVersionSelect.version}` + connectOptions.botVersion = autoVersionSelect.version } else { - initialLoadingText = 'Connecting to server' + initialLoadingText = 'We have no idea what to do' } setLoadingScreenStatus(initialLoadingText) @@ -454,6 +472,10 @@ export async function connect (connectOptions: ConnectOptions) { clientDataStream = await getWsProtocolStream(connectOptions.viewerWsConnect) } + if (connectOptions.botVersion) { + miscUiState.loadedDataVersion = connectOptions.botVersion + } + bot = mineflayer.createBot({ host: server.host, port: server.port ? +server.port : undefined, @@ -523,10 +545,6 @@ export async function connect (connectOptions: ConnectOptions) { closeTimeout: 240 * 1000, respawn: options.autoRespawn, maxCatchupTicks: 0, - async versionSelectedHook (client) { - await downloadMcData(client.version) - setLoadingScreenStatus(initialLoadingText) - }, 'mapDownloader-saveToFile': false, // "mapDownloader-saveInternal": false, // do not save into memory, todo must be implemeneted as we do really care of ram }) as unknown as typeof __type_bot @@ -634,6 +652,9 @@ export async function connect (connectOptions: ConnectOptions) { bot.on('end', (endReason) => { if (ended) return console.log('disconnected for', endReason) + if (endReason === 'socketClosed') { + endReason = 'Connection with server lost' + } setLoadingScreenStatus(`You have been disconnected from the server. End reason: ${endReason}`, true) onPossibleErrorDisconnect() destroyAll() diff --git a/src/mineflayer/mc-protocol.ts b/src/mineflayer/mc-protocol.ts new file mode 100644 index 000000000..583a98bb4 --- /dev/null +++ b/src/mineflayer/mc-protocol.ts @@ -0,0 +1,15 @@ +import { Client } from 'minecraft-protocol' +import { appQueryParams } from '../appParams' +import { validatePacket } from './minecraft-protocol-extra' + +customEvents.on('mineflayerBotCreated', () => { + // todo move more code here + if (!appQueryParams.noPacketsValidation) { + (bot._client as unknown as Client).on('packet', (data, packetMeta, buffer, fullBuffer) => { + validatePacket(packetMeta.name, data, fullBuffer, true) + }); + (bot._client as unknown as Client).on('writePacket', (name, params) => { + validatePacket(name, params, Buffer.alloc(0), false) + }) + } +}) diff --git a/src/mineflayer/minecraft-protocol-extra.ts b/src/mineflayer/minecraft-protocol-extra.ts new file mode 100644 index 000000000..65ed6031d --- /dev/null +++ b/src/mineflayer/minecraft-protocol-extra.ts @@ -0,0 +1,81 @@ +import EventEmitter from 'events' +import clientAutoVersion from 'minecraft-protocol/src/client/autoVersion' + +export const pingServerVersion = async (ip: string, port?: number, preferredVersion?: string) => { + const fakeClient = new EventEmitter() as any + fakeClient.on('error', (err) => { + throw new Error(err.message ?? err) + }) + const options = { + host: ip, + port, + version: preferredVersion, + noPongTimeout: Infinity // disable timeout + } + // let latency = 0 + // fakeClient.autoVersionHooks = [(res) => { + // latency = res.latency + // }] + + // TODO! use client.socket.destroy() instead of client.end() for faster cleanup + await clientAutoVersion(fakeClient, options) + + await new Promise((resolve, reject) => { + fakeClient.once('connect_allowed', resolve) + }) + return { + version: fakeClient.version, + // latency, + } +} + +const MAX_PACKET_SIZE = 2_097_152 // 2mb +const MAX_PACKET_DEPTH = 20 + +export const validatePacket = (name: string, data: any, fullBuffer: Buffer, isFromServer: boolean) => { + // todo find out why chat is so slow with react + if (!isFromServer) return + + if (fullBuffer.length > MAX_PACKET_SIZE) { + console.groupCollapsed(`Packet ${name} is too large: ${fullBuffer.length} bytes`) + console.log(data) + console.groupEnd() + throw new Error(`Packet ${name} is too large: ${fullBuffer.length} bytes`) + } + + // todo count total number of objects instead of max depth + const maxDepth = getObjectMaxDepth(data) + if (maxDepth > MAX_PACKET_DEPTH) { + console.groupCollapsed(`Packet ${name} have too many nested objects: ${maxDepth}`) + console.log(data) + console.groupEnd() + throw new Error(`Packet ${name} have too many nested objects: ${maxDepth}`) + } +} + +function getObjectMaxDepth (obj: unknown, currentDepth = 0): number { + // Base case: null or primitive types have depth 0 + if (obj === null || typeof obj !== 'object' || obj instanceof Buffer) { + return currentDepth + } + + // Handle arrays and objects + let maxDepth = currentDepth + + if (Array.isArray(obj)) { + // For arrays, check each element + for (const item of obj) { + const depth = getObjectMaxDepth(item, currentDepth + 1) + maxDepth = Math.max(maxDepth, depth) + } + } else { + // For objects, check each value + // eslint-disable-next-line guard-for-in + for (const key in obj) { + const depth = getObjectMaxDepth(obj[key], currentDepth + 1) + maxDepth = Math.max(maxDepth, depth) + } + } + + return maxDepth +} diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx index 2ee07d5c8..c3ede4078 100644 --- a/src/optionsGuiScheme.tsx +++ b/src/optionsGuiScheme.tsx @@ -12,6 +12,8 @@ import { openFilePicker, resetLocalStorageWithoutWorld } from './browserfs' import { completeTexturePackInstall, getResourcePackNames, resourcePackState, uninstallTexturePack } from './resourcePack' import { downloadPacketsReplay, packetsReplaceSessionState } from './packetsReplay' import { showOptionsModal } from './react/SelectOption' +import supportedVersions from './supportedVersions.mjs' +import { getVersionAutoSelect } from './connect' export const guiOptionsScheme: { [t in OptionsGroupType]: Array<{ [K in keyof AppOptions]?: Partial> } & { custom? }> @@ -480,6 +482,34 @@ export const guiOptionsScheme: { ], }, }, + { + custom () { + const { serversAutoVersionSelect } = useSnapshot(options) + const allVersions = [...supportedVersions, 'latest', 'auto'] + const currentIndex = allVersions.indexOf(serversAutoVersionSelect) + + const getDisplayValue = (version: string) => { + const versionAutoSelect = getVersionAutoSelect(version) + if (version === 'latest') return `latest (${versionAutoSelect})` + if (version === 'auto') return `auto (${versionAutoSelect})` + return version + } + + return
+ { + options.serversAutoVersionSelect = allVersions[newVal] + }} + /> +
+ }, + }, ], } export type OptionsGroupType = 'main' | 'render' | 'interface' | 'controls' | 'sound' | 'advanced' | 'VR' diff --git a/src/optionsStorage.ts b/src/optionsStorage.ts index 24aaff41a..12188a17b 100644 --- a/src/optionsStorage.ts +++ b/src/optionsStorage.ts @@ -53,6 +53,7 @@ const defaultOptions = { serverResourcePacks: 'prompt' as 'prompt' | 'always' | 'never', handDisplay: false, packetsLoggerPreset: 'all' as 'all' | 'no-buffers', + serversAutoVersionSelect: 'auto' as 'auto' | 'latest' | '1.20.4' | string, // antiAliasing: false, @@ -84,7 +85,6 @@ const defaultOptions = { autoParkour: false, vrSupport: true, // doesn't directly affect the VR mode, should only disable the button which is annoying to android users renderDebug: (isDev ? 'advanced' : 'basic') as 'none' | 'advanced' | 'basic', - autoVersionSelect: '1.20.4', // advanced bot options autoRespawn: false, diff --git a/src/shims/minecraftData.ts b/src/shims/minecraftData.ts index 6edb5f486..09124771a 100644 --- a/src/shims/minecraftData.ts +++ b/src/shims/minecraftData.ts @@ -1,6 +1,6 @@ import { versionToNumber } from 'prismarine-viewer/viewer/prepare/utils' import JsonOptimizer from '../optimizeJson' -import minecraftInitialDataJson from '../../generated/minecraft-initial-data.json' +// import minecraftInitialDataJson from '../../generated/minecraft-initial-data.json' import { toMajorVersion } from '../utils' const customResolver = () => { @@ -30,9 +30,9 @@ const cacheTtl = 30 * 1000 const cache = new Map() const cacheTime = new Map() const possiblyGetFromCache = (version: string) => { - if (minecraftInitialDataJson[version] && !optimizedDataResolver.resolvedData) { - return minecraftInitialDataJson[version] - } + // if (minecraftInitialDataJson[version] && !optimizedDataResolver.resolvedData) { + // return minecraftInitialDataJson[version] + // } if (cache.has(version)) { return cache.get(version) } diff --git a/src/shims/patchShims.ts b/src/shims/patchShims.ts new file mode 100644 index 000000000..1890edf63 --- /dev/null +++ b/src/shims/patchShims.ts @@ -0,0 +1,10 @@ +import { EventEmitter } from 'events' + +const oldEmit = EventEmitter.prototype.emit +EventEmitter.prototype.emit = function (...args) { + if (args[0] === 'error' && !this._events.error) { + console.log('Unhandled error event', args.slice(1)) + args[1] = { message: String(args[1]) } + } + return oldEmit.apply(this, args) +}