diff --git a/rsbuild.config.ts b/rsbuild.config.ts index e264f6b73..42d6867b3 100644 --- a/rsbuild.config.ts +++ b/rsbuild.config.ts @@ -139,6 +139,13 @@ const appConfig = defineConfig({ // 50kb limit for data uri dataUriLimit: SINGLE_FILE_BUILD ? 1 * 1024 * 1024 * 1024 : 50 * 1024 }, + performance: { + // prefetch: { + // include(filename) { + // return filename.includes('mc-data') || filename.includes('mc-assets') + // }, + // }, + }, source: { entry: { index: './src/index.ts', @@ -154,7 +161,7 @@ const appConfig = defineConfig({ 'process.platform': '"browser"', 'process.env.GITHUB_URL': JSON.stringify(`https://github.com/${process.env.GITHUB_REPOSITORY || `${process.env.VERCEL_GIT_REPO_OWNER}/${process.env.VERCEL_GIT_REPO_SLUG}` || githubRepositoryFallback}`), - 'process.env.DEPS_VERSIONS': JSON.stringify({}), + 'process.env.ALWAYS_MINIMAL_SERVER_UI': JSON.stringify(process.env.ALWAYS_MINIMAL_SERVER_UI), 'process.env.RELEASE_TAG': JSON.stringify(releaseTag), 'process.env.RELEASE_LINK': JSON.stringify(releaseLink), 'process.env.RELEASE_CHANGELOG': JSON.stringify(releaseChangelog), @@ -190,7 +197,7 @@ const appConfig = defineConfig({ childProcess.execSync('tsx ./scripts/optimizeBlockCollisions.ts', { stdio: 'inherit' }) } // childProcess.execSync(['tsx', './scripts/genLargeDataAliases.ts', ...(SINGLE_FILE_BUILD ? ['--compressed'] : [])].join(' '), { stdio: 'inherit' }) - genLargeDataAliases(SINGLE_FILE_BUILD) + genLargeDataAliases(SINGLE_FILE_BUILD || process.env.ALWAYS_COMPRESS_LARGE_DATA === 'true') fsExtra.copySync('./node_modules/mc-assets/dist/other-textures/latest/entity', './dist/textures/entity') fsExtra.copySync('./assets/background', './dist/background') fs.copyFileSync('./assets/favicon.png', './dist/favicon.png') diff --git a/scripts/genLargeDataAliases.ts b/scripts/genLargeDataAliases.ts index 0cf206df0..2372dbfd8 100644 --- a/scripts/genLargeDataAliases.ts +++ b/scripts/genLargeDataAliases.ts @@ -16,7 +16,8 @@ export const genLargeDataAliases = async (isCompressed: boolean) => { let str = `${decoderCode}\nexport const importLargeData = async (mod: ${Object.keys(modules).map(x => `'${x}'`).join(' | ')}) => {\n` for (const [module, { compressed, raw }] of Object.entries(modules)) { - let importCode = `(await import('${isCompressed ? compressed : raw}')).default`; + const chunkName = module === 'mcData' ? 'mc-data' : 'mc-assets'; + let importCode = `(await import(/* webpackChunkName: "${chunkName}" */ '${isCompressed ? compressed : raw}')).default`; if (isCompressed) { importCode = `JSON.parse(decompressFromBase64(${importCode}))` } @@ -30,6 +31,8 @@ export const genLargeDataAliases = async (isCompressed: boolean) => { const decoderCode = /* ts */ ` import pako from 'pako'; +globalThis.pako = { inflate: pako.inflate.bind(pako) } + function decompressFromBase64(input) { console.time('decompressFromBase64') // Decode the Base64 string diff --git a/scripts/makeOptimizedMcData.mjs b/scripts/makeOptimizedMcData.mjs index 05948cf27..0b5752d89 100644 --- a/scripts/makeOptimizedMcData.mjs +++ b/scripts/makeOptimizedMcData.mjs @@ -6,8 +6,8 @@ import { dirname } from 'node:path' import supportedVersions from '../src/supportedVersions.mjs' import { gzipSizeFromFileSync } from 'gzip-size' import fs from 'fs' -import {default as _JsonOptimizer} from '../src/optimizeJson' -import { gzipSync } from 'zlib'; +import { default as _JsonOptimizer } from '../src/optimizeJson' +import { gzipSync } from 'zlib' import MinecraftData from 'minecraft-data' import MCProtocol from 'minecraft-protocol' @@ -21,12 +21,12 @@ const require = Module.createRequire(import.meta.url) const dataPaths = require('minecraft-data/minecraft-data/data/dataPaths.json') -function toMajor (version) { +function toMajor(version) { const [a, b] = (version + '').split('.') return `${a}.${b}` } -const versions = {} +let versions = {} const dataTypes = new Set() for (const [version, dataSet] of Object.entries(dataPaths.pc)) { @@ -42,6 +42,31 @@ const versionToNumber = (ver) => { return +`${x.padStart(2, '0')}${y.padStart(2, '0')}${z.padStart(2, '0')}` } +// Version clipping support +const minVersion = process.env.MIN_MC_VERSION +const maxVersion = process.env.MAX_MC_VERSION + +// Filter versions based on MIN_VERSION and MAX_VERSION if provided +if (minVersion || maxVersion) { + const filteredVersions = {} + const minVersionNum = minVersion ? versionToNumber(minVersion) : 0 + const maxVersionNum = maxVersion ? versionToNumber(maxVersion) : Infinity + + for (const [version, dataSet] of Object.entries(versions)) { + const versionNum = versionToNumber(version) + if (versionNum >= minVersionNum && versionNum <= maxVersionNum) { + filteredVersions[version] = dataSet + } + } + + versions = filteredVersions + + console.log(`Version clipping applied: ${minVersion || 'none'} to ${maxVersion || 'none'}`) + console.log(`Processing ${Object.keys(versions).length} versions:`, Object.keys(versions).sort((a, b) => versionToNumber(a) - versionToNumber(b))) +} + +console.log('Bundling version range:', Object.keys(versions)[0], 'to', Object.keys(versions).at(-1)) + // if not included here (even as {}) will not be bundled & accessible! // const compressedOutput = !!process.env.SINGLE_FILE_BUILD const compressedOutput = true @@ -57,18 +82,20 @@ const dataTypeBundling2 = { } } const dataTypeBundling = { - language: { + language: process.env.SKIP_MC_DATA_LANGUAGE === 'true' ? { + raw: {} + } : { ignoreRemoved: true, ignoreChanges: true }, blocks: { arrKey: 'name', - processData (current, prev) { + processData(current, prev) { for (const block of current) { if (block.transparent) { const forceOpaque = block.name.includes('shulker_box') || block.name.match(/^double_.+_slab\d?$/) || ['melon_block', 'lit_pumpkin', 'lit_redstone_ore', 'lit_furnace'].includes(block.name) - const prevBlock = prev?.find(x => x.name === block.name); + const prevBlock = prev?.find(x => x.name === block.name) if (forceOpaque || (prevBlock && !prevBlock.transparent)) { block.transparent = false } @@ -136,7 +163,9 @@ const dataTypeBundling = { blockLoot: { arrKey: 'block' }, - recipes: { + recipes: process.env.SKIP_MC_DATA_RECIPES === 'true' ? { + raw: {} + } : { raw: true // processData: processRecipes }, @@ -150,7 +179,7 @@ const dataTypeBundling = { // } } -function processRecipes (current, prev, getData, version) { +function processRecipes(current, prev, getData, version) { // can require the same multiple times per different versions if (current._proccessed) return const items = getData('items') @@ -242,30 +271,39 @@ for (const [i, [version, dataSet]] of versionsArr.reverse().entries()) { for (const [dataType, dataPath] of Object.entries(dataSet)) { const config = dataTypeBundling[dataType] if (!config) continue - if (dataType === 'blockCollisionShapes' && versionToNumber(version) >= versionToNumber('1.13')) { - // contents += ` get ${dataType} () { return window.globalGetCollisionShapes?.("${version}") },\n` - continue - } + const ignoreCollisionShapes = dataType === 'blockCollisionShapes' && versionToNumber(version) >= versionToNumber('1.13') + let injectCode = '' - const getData = (type) => { + const getRealData = (type) => { const loc = `minecraft-data/data/${dataSet[type]}/` const dataPathAbsolute = require.resolve(`minecraft-data/${loc}${type}`) // const data = fs.readFileSync(dataPathAbsolute, 'utf8') const dataRaw = require(dataPathAbsolute) return dataRaw } - const dataRaw = getData(dataType) + const dataRaw = getRealData(dataType) let rawData = dataRaw if (config.raw) { rawDataVersions[dataType] ??= {} rawDataVersions[dataType][version] = rawData - rawData = dataRaw + if (config.raw === true) { + rawData = dataRaw + } else { + rawData = config.raw + } + + if (ignoreCollisionShapes && dataType === 'blockCollisionShapes') { + rawData = { + blocks: {}, + shapes: {} + } + } } else { if (!diffSources[dataType]) { diffSources[dataType] = new JsonOptimizer(config.arrKey, config.ignoreChanges, config.ignoreRemoved) } try { - config.processData?.(dataRaw, previousData[dataType], getData, version) + config.processData?.(dataRaw, previousData[dataType], getRealData, version) diffSources[dataType].recordDiff(version, dataRaw) injectCode = `restoreDiff(sources, ${JSON.stringify(dataType)}, ${JSON.stringify(version)})` } catch (err) { @@ -297,16 +335,16 @@ console.log('total size (mb)', totalSize / 1024 / 1024) console.log( 'size per data type (mb, %)', Object.fromEntries(Object.entries(sizePerDataType).map(([dataType, size]) => { - return [dataType, [size / 1024 / 1024, Math.round(size / totalSize * 100)]]; + return [dataType, [size / 1024 / 1024, Math.round(size / totalSize * 100)]] }).sort((a, b) => { //@ts-ignore - return b[1][1] - a[1][1]; + return b[1][1] - a[1][1] })) ) function compressToBase64(input) { - const buffer = gzipSync(input); - return buffer.toString('base64'); + const buffer = gzipSync(input) + return buffer.toString('base64') } const filePath = './generated/minecraft-data-optimized.json' diff --git a/src/downloadAndOpenFile.ts b/src/downloadAndOpenFile.ts index 1e703369d..1ff318ff8 100644 --- a/src/downloadAndOpenFile.ts +++ b/src/downloadAndOpenFile.ts @@ -11,6 +11,12 @@ export const getFixedFilesize = (bytes: number) => { return prettyBytes(bytes, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) } +export const isInterestedInDownload = () => { + const { map, texturepack, replayFileUrl } = appQueryParams + const { mapDir } = appQueryParamsArray + return !!map || !!texturepack || !!replayFileUrl || !!mapDir +} + const inner = async () => { const { map, texturepack, replayFileUrl } = appQueryParams const { mapDir } = appQueryParamsArray diff --git a/src/env.d.ts b/src/env.d.ts index 9b3e9774d..e565fcec3 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -2,30 +2,36 @@ declare namespace NodeJS { interface ProcessEnv { // Build configuration NODE_ENV: 'development' | 'production' - SINGLE_FILE_BUILD?: string + MIN_MC_VERSION?: string + MAX_MC_VERSION?: string + ALWAYS_COMPRESS_LARGE_DATA?: 'true' | 'false' + SINGLE_FILE_BUILD?: 'true' | 'false' WS_PORT?: string - DISABLE_SERVICE_WORKER?: string + DISABLE_SERVICE_WORKER?: 'true' | 'false' CONFIG_JSON_SOURCE?: 'BUNDLED' | 'REMOTE' LOCAL_CONFIG_FILE?: string BUILD_VERSION?: string - // GitHub and Vercel related + // Build internals GITHUB_REPOSITORY?: string VERCEL_GIT_REPO_OWNER?: string VERCEL_GIT_REPO_SLUG?: string - // UI and Features + // UI MAIN_MENU_LINKS?: string + ALWAYS_MINIMAL_SERVER_UI?: 'true' | 'false' + + // App features ENABLE_COOKIE_STORAGE?: string COOKIE_STORAGE_PREFIX?: string - // Release information + // Build info. Release information RELEASE_TAG?: string RELEASE_LINK?: string RELEASE_CHANGELOG?: string - // Other configurations - DEPS_VERSIONS?: string + // Build info INLINED_APP_CONFIG?: string + GITHUB_URL?: string } } diff --git a/src/index.ts b/src/index.ts index 185caab6f..7a553ca7a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,7 +29,7 @@ import './reactUi' import { lockUrl, onBotCreate } from './controls' import './dragndrop' import { possiblyCleanHandle } from './browserfs' -import downloadAndOpenFile from './downloadAndOpenFile' +import downloadAndOpenFile, { isInterestedInDownload } from './downloadAndOpenFile' import fs from 'fs' import net, { Socket } from 'net' @@ -97,6 +97,7 @@ import { registerOpenBenchmarkListener } from './benchmark' import { tryHandleBuiltinCommand } from './builtinCommands' import { loadingTimerState } from './react/LoadingTimer' import { loadPluginsIntoWorld } from './react/CreateWorldProvider' +import { getCurrentProxy, getCurrentUsername } from './react/ServersList' window.debug = debug window.beforeRenderFrame = [] @@ -166,6 +167,7 @@ export async function connect (connectOptions: ConnectOptions) { }) } + appStatusState.showReconnect = false loadingTimerState.loading = true loadingTimerState.start = Date.now() miscUiState.hasErrors = false @@ -880,37 +882,7 @@ export async function connect (connectOptions: ConnectOptions) { } } -const reconnectOptions = sessionStorage.getItem('reconnectOptions') ? JSON.parse(sessionStorage.getItem('reconnectOptions')!) : undefined - listenGlobalEvents() -const unsubscribe = subscribe(miscUiState, async () => { - if (miscUiState.fsReady && miscUiState.appConfig) { - unsubscribe() - if (reconnectOptions) { - sessionStorage.removeItem('reconnectOptions') - if (Date.now() - reconnectOptions.timestamp < 1000 * 60 * 2) { - void connect(reconnectOptions.value) - } - } else { - if (appQueryParams.singleplayer === '1' || appQueryParams.sp === '1') { - loadSingleplayer({}, { - worldFolder: undefined, - ...appQueryParams.version ? { version: appQueryParams.version } : {} - }) - } - if (appQueryParams.loadSave) { - const savePath = `/data/worlds/${appQueryParams.loadSave}` - try { - await fs.promises.stat(savePath) - } catch (err) { - alert(`Save ${savePath} not found`) - return - } - await loadInMemorySave(savePath) - } - } - } -}) // #region fire click event on touch as we disable default behaviors let activeTouch: { touch: Touch, elem: HTMLElement, start: number } | undefined @@ -946,90 +918,153 @@ document.body.addEventListener('touchstart', (e) => { }, { passive: false }) // #endregion -// qs open actions -if (!reconnectOptions) { - downloadAndOpenFile().then((downloadAction) => { - if (downloadAction) return - if (appQueryParams.reconnect && process.env.NODE_ENV === 'development') { - const lastConnect = JSON.parse(localStorage.lastConnectOptions ?? {}) +// immediate game enter actions: reconnect or URL QS +const maybeEnterGame = () => { + const waitForConfigFsLoad = (fn: () => void) => { + let unsubscribe: () => void | undefined + const checkDone = () => { + if (miscUiState.fsReady && miscUiState.appConfig) { + fn() + unsubscribe?.() + return true + } + return false + } + + if (!checkDone()) { + const text = miscUiState.appConfig ? 'Loading' : 'Loading config' + setLoadingScreenStatus(text) + unsubscribe = subscribe(miscUiState, checkDone) + } + } + + const reconnectOptions = sessionStorage.getItem('reconnectOptions') ? JSON.parse(sessionStorage.getItem('reconnectOptions')!) : undefined + + if (reconnectOptions) { + sessionStorage.removeItem('reconnectOptions') + if (Date.now() - reconnectOptions.timestamp < 1000 * 60 * 2) { + return waitForConfigFsLoad(async () => { + void connect(reconnectOptions.value) + }) + } + } + + if (appQueryParams.reconnect && process.env.NODE_ENV === 'development') { + const lastConnect = JSON.parse(localStorage.lastConnectOptions ?? {}) + return waitForConfigFsLoad(async () => { void connect({ botVersion: appQueryParams.version ?? undefined, ...lastConnect, ip: appQueryParams.ip || undefined }) - return - } - if (appQueryParams.ip || appQueryParams.proxy) { - const waitAppConfigLoad = !appQueryParams.proxy - const openServerEditor = () => { - hideModal() - if (appQueryParams.onlyConnect) { - showModal({ reactType: 'only-connect-server' }) - } else { - showModal({ reactType: 'editServer' }) - } - } - showModal({ reactType: 'empty' }) - if (waitAppConfigLoad) { - const unsubscribe = subscribe(miscUiState, checkCanDisplay) - checkCanDisplay() - // eslint-disable-next-line no-inner-declarations - function checkCanDisplay () { - if (miscUiState.appConfig) { - unsubscribe() - openServerEditor() - return true - } - } - } else { - openServerEditor() + }) + } + + if (appQueryParams.singleplayer === '1' || appQueryParams.sp === '1') { + return waitForConfigFsLoad(async () => { + loadSingleplayer({}, { + worldFolder: undefined, + ...appQueryParams.version ? { version: appQueryParams.version } : {} + }) + }) + } + if (appQueryParams.loadSave) { + const enterSave = async () => { + const savePath = `/data/worlds/${appQueryParams.loadSave}` + try { + await fs.promises.stat(savePath) + await loadInMemorySave(savePath) + } catch (err) { + alert(`Save ${savePath} not found`) } } + return waitForConfigFsLoad(enterSave) + } - void Promise.resolve().then(() => { - // try to connect to peer - const peerId = appQueryParams.connectPeer - const peerOptions = {} as ConnectPeerOptions - if (appQueryParams.server) { - peerOptions.server = appQueryParams.server - } - const version = appQueryParams.peerVersion - if (peerId) { - let username: string | null = options.guestUsername - if (options.askGuestName) username = prompt('Enter your username', username) - if (!username) return - options.guestUsername = username + if (appQueryParams.ip || appQueryParams.proxy) { + const waitAppConfigLoad = !appQueryParams.proxy + const openServerAction = () => { + if (appQueryParams.autoConnect && miscUiState.appConfig?.allowAutoConnect) { void connect({ - username, - botVersion: version || undefined, - peerId, - peerOptions + server: appQueryParams.ip, + proxy: getCurrentProxy(), + botVersion: appQueryParams.version ?? undefined, + username: getCurrentUsername()!, }) + return } - }) - if (appQueryParams.serversList && !appQueryParams.ip) { - showModal({ reactType: 'serversList' }) + setLoadingScreenStatus(undefined) + if (appQueryParams.onlyConnect || process.env.ALWAYS_MINIMAL_SERVER_UI === 'true') { + showModal({ reactType: 'only-connect-server' }) + } else { + showModal({ reactType: 'editServer' }) + } } - const viewerWsConnect = appQueryParams.viewerConnect - if (viewerWsConnect) { - void connect({ - username: `viewer-${Math.random().toString(36).slice(2, 10)}`, - viewerWsConnect, - }) + // showModal({ reactType: 'empty' }) + if (waitAppConfigLoad) { + return waitForConfigFsLoad(openServerAction) } + openServerAction() + return + } - if (appQueryParams.modal) { - const modals = appQueryParams.modal.split(',') - for (const modal of modals) { - showModal({ reactType: modal }) - } + if (appQueryParams.connectPeer) { + // try to connect to peer + const peerId = appQueryParams.connectPeer + const peerOptions = {} as ConnectPeerOptions + if (appQueryParams.server) { + peerOptions.server = appQueryParams.server } - }, (err) => { - console.error(err) - alert(`Something went wrong: ${err}`) - }) + const version = appQueryParams.peerVersion + let username: string | null = options.guestUsername + if (options.askGuestName) username = prompt('Enter your username to connect to peer', username) + if (!username) return + options.guestUsername = username + void connect({ + username, + botVersion: version || undefined, + peerId, + peerOptions + }) + return + + } + + if (appQueryParams.viewerConnect) { + void connect({ + username: `viewer-${Math.random().toString(36).slice(2, 10)}`, + viewerWsConnect: appQueryParams.viewerConnect, + }) + return + } + + if (appQueryParams.modal) { + const modals = appQueryParams.modal.split(',') + for (const modal of modals) { + showModal({ reactType: modal }) + } + return + } + + if (appQueryParams.serversList && !miscUiState.appConfig?.appParams?.serversList) { + // open UI only if it's in URL + showModal({ reactType: 'serversList' }) + } + + if (isInterestedInDownload()) { + void downloadAndOpenFile() + } + + void possiblyHandleStateVariable() +} + +try { + maybeEnterGame() +} catch (err) { + console.error(err) + alert(`Something went wrong: ${err}`) } // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion @@ -1040,6 +1075,5 @@ if (initialLoader) { } window.pageLoaded = true -void possiblyHandleStateVariable() appViewer.waitBackendLoadPromises.push(appStartup()) registerOpenBenchmarkListener() diff --git a/src/inventoryWindows.ts b/src/inventoryWindows.ts index c9f60d592..1e17db145 100644 --- a/src/inventoryWindows.ts +++ b/src/inventoryWindows.ts @@ -522,7 +522,7 @@ const getResultingRecipe = (slots: Array, gridRows: number) => { type Result = RecipeItem | undefined let shapelessResult: Result let shapeResult: Result - outer: for (const [id, recipeVariants] of Object.entries(loadedData.recipes)) { + outer: for (const [id, recipeVariants] of Object.entries(loadedData.recipes ?? {})) { for (const recipeVariant of recipeVariants) { if ('inShape' in recipeVariant && equals(currentShape, recipeVariant.inShape as number[][])) { shapeResult = recipeVariant.result! @@ -550,7 +550,7 @@ const getAllItemRecipes = (itemName: string) => { const item = loadedData.itemsByName[itemName] if (!item) return const itemId = item.id - const recipes = loadedData.recipes[itemId] + const recipes = loadedData.recipes?.[itemId] if (!recipes) return const results = [] as Array<{ result: Item, @@ -595,7 +595,7 @@ const getAllItemUsages = (itemName: string) => { if (!item) return const foundRecipeIds = [] as string[] - for (const [id, recipes] of Object.entries(loadedData.recipes)) { + for (const [id, recipes] of Object.entries(loadedData.recipes ?? {})) { for (const recipe of recipes) { if ('inShape' in recipe) { if (recipe.inShape.some(row => row.includes(item.id))) { diff --git a/src/react/AddServerOrConnect.tsx b/src/react/AddServerOrConnect.tsx index 08e4d69e0..d478b3e76 100644 --- a/src/react/AddServerOrConnect.tsx +++ b/src/react/AddServerOrConnect.tsx @@ -29,10 +29,9 @@ interface Props { accounts?: string[] authenticatedAccounts?: number versions?: string[] - allowAutoConnect?: boolean } -export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQs, onQsConnect, placeholders, accounts, versions, allowAutoConnect }: Props) => { +export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQs, onQsConnect, placeholders, accounts, versions }: Props) => { const isSmallHeight = !usePassesScaledDimensions(null, 350) const qsParamName = parseQs ? appQueryParams.name : undefined const qsParamIp = parseQs ? appQueryParams.ip : undefined @@ -40,7 +39,6 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ const qsParamProxy = parseQs ? appQueryParams.proxy : undefined const qsParamUsername = parseQs ? appQueryParams.username : undefined const qsParamLockConnect = parseQs ? appQueryParams.lockConnect : undefined - const qsParamAutoConnect = parseQs ? appQueryParams.autoConnect : undefined const parsedQsIp = parseServerAddress(qsParamIp) const parsedInitialIp = parseServerAddress(initialData?.ip) @@ -118,12 +116,6 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ } } - useEffect(() => { - if (qsParamAutoConnect && qsParamIp && qsParamVersion && allowAutoConnect) { - onQsConnect?.(commonUseOptions) - } - }, []) - const displayConnectButton = qsParamIp const serverExamples = ['example.com:25565', 'play.hypixel.net', 'ws://play.pcm.gg'] // pick random example @@ -231,7 +223,7 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ Cancel - {displayConnectButton ? 'Save' : Save} + {displayConnectButton ? translate('Save') : {translate('Save')}} } {displayConnectButton && ( @@ -246,7 +238,7 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ onQsConnect?.(commonUseOptions) }} > - Connect + {translate('Connect')} )} diff --git a/src/react/ServersListProvider.tsx b/src/react/ServersListProvider.tsx index d95d4e9e2..2509db74d 100644 --- a/src/react/ServersListProvider.tsx +++ b/src/react/ServersListProvider.tsx @@ -156,13 +156,22 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL const isWebSocket = server.ip.startsWith('ws://') || server.ip.startsWith('wss://') let data if (isWebSocket) { - const pingResult = await getServerInfo(server.ip, undefined, undefined, true) - console.log('pingResult.fullInfo.description', pingResult.fullInfo.description) - data = { - formattedText: pingResult.fullInfo.description, - textNameRight: `ws ${pingResult.latency}ms`, - textNameRightGrayed: `${pingResult.fullInfo.players?.online ?? '??'}/${pingResult.fullInfo.players?.max ?? '??'}`, - offline: false + try { + const pingResult = await getServerInfo(server.ip, undefined, undefined, true) + console.log('pingResult.fullInfo.description', pingResult.fullInfo.description) + data = { + formattedText: pingResult.fullInfo.description, + textNameRight: `ws ${pingResult.latency}ms`, + textNameRightGrayed: `${pingResult.fullInfo.players?.online ?? '??'}/${pingResult.fullInfo.players?.max ?? '??'}`, + offline: false + } + } catch (err) { + data = { + formattedText: 'Failed to connect', + textNameRight: '', + textNameRightGrayed: '', + offline: true + } } } else { data = await fetchServerStatus(server.ip, /* signal */undefined, server.versionOverride) // DONT ADD SIGNAL IT WILL CRUSH JS RUNTIME @@ -217,7 +226,6 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL }) const editModalJsx = isEditScreenModal ?