Skip to content

Commit

Permalink
feat: Add WebSocket server support for direct connections! (no proxy …
Browse files Browse the repository at this point in the history
…url is used then)
  • Loading branch information
zardoy committed Feb 4, 2025
1 parent 32acb55 commit b5a16d5
Show file tree
Hide file tree
Showing 12 changed files with 151 additions and 39 deletions.
3 changes: 3 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
"peerJsServer": "",
"peerJsServerFallback": "https://p2p.mcraft.fun",
"promoteServers": [
{
"ip": "ws://play.mcraft.fun"
},
{
"ip": "kaboom.pw",
"version": "1.20.3",
Expand Down
6 changes: 4 additions & 2 deletions src/connect.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
// 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'

Expand Down Expand Up @@ -49,6 +47,10 @@ export const downloadMcDataOnConnect = async (version: string) => {
// miscUiState.loadedDataVersion = version
}

export const downloadAllMinecraftData = async () => {
await window._LOAD_MC_DATA()
}

const loadFonts = async () => {
const FONT_FAMILY = 'mojangles'
if (!document.fonts.check(`1em ${FONT_FAMILY}`)) {
Expand Down
15 changes: 1 addition & 14 deletions src/globalState.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
//@ts-check

import { proxy, ref, subscribe } from 'valtio'
import { WorldWarp } from 'flying-squid/dist/lib/modules/warps'
import { pointerLock } from './utils'
import type { WorldWarp } from 'flying-squid/dist/lib/modules/warps'
import type { OptionsGroupType } from './optionsGuiScheme'

// todo: refactor structure with support of hideNext=false
Expand All @@ -26,16 +25,6 @@ export const activeModalStacks: Record<string, Modal[]> = {}

window.activeModalStack = activeModalStack

subscribe(activeModalStack, () => {
if (activeModalStack.length === 0) {
if (isGameActive(false)) {
void pointerLock.requestPointerLock()
}
} else {
document.exitPointerLock?.()
}
})

/**
* @returns true if operation was successful
*/
Expand Down Expand Up @@ -169,5 +158,3 @@ export const gameAdditionalState = proxy({
})

window.gameAdditionalState = gameAdditionalState

// todo restore auto-save on interval for player data! (or implement it in flying squid since there is already auto-save for world)
31 changes: 22 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ import { saveToBrowserMemory } from './react/PauseScreen'
import { ViewerWrapper } from 'prismarine-viewer/viewer/lib/viewerWrapper'
import './devReload'
import './water'
import { ConnectOptions, downloadMcDataOnConnect, getVersionAutoSelect, downloadOtherGameData } from './connect'
import { ConnectOptions, downloadMcDataOnConnect, getVersionAutoSelect, downloadOtherGameData, downloadAllMinecraftData } from './connect'
import { ref, subscribe } from 'valtio'
import { signInMessageState } from './react/SignInMessageProvider'
import { updateAuthenticatedAccountData, updateLoadedServerData } from './react/ServersListProvider'
Expand All @@ -99,9 +99,11 @@ import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer'
import './mobileShim'
import { parseFormattedMessagePacket } from './botUtils'
import { getViewerVersionData, getWsProtocolStream } from './viewerConnector'
import { getWebsocketStream } from './mineflayer/websocket-core'
import { appQueryParams, appQueryParamsArray } from './appParams'
import { updateCursor } from './cameraRotationControls'
import { pingServerVersion } from './mineflayer/minecraft-protocol-extra'
import { getServerInfo } from './mineflayer/mc-protocol'

Check failure on line 106 in src/index.ts

View workflow job for this annotation

GitHub Actions / build-and-deploy

'./mineflayer/mc-protocol' imported multiple times

window.debug = debug
window.THREE = THREE
Expand Down Expand Up @@ -262,7 +264,8 @@ export async function connect (connectOptions: ConnectOptions) {
miscUiState.singleplayer = singleplayer
miscUiState.flyingSquid = singleplayer || p2pMultiplayer
const { renderDistance: renderDistanceSingleplayer, multiplayerRenderDistance } = options
const server = cleanConnectIp(connectOptions.server, '25565')
const isWebSocket = connectOptions.server?.startsWith('ws://') || connectOptions.server?.startsWith('wss://')
const server = isWebSocket ? { host: connectOptions.server, port: undefined } : cleanConnectIp(connectOptions.server, '25565')
if (connectOptions.proxy?.startsWith(':')) {
connectOptions.proxy = `${location.protocol}//${location.hostname}${connectOptions.proxy}`
}
Expand Down Expand Up @@ -352,9 +355,10 @@ export async function connect (connectOptions: ConnectOptions) {
signal: errorAbortController.signal
})

if (proxy && !connectOptions.viewerWsConnect) {
console.log(`using proxy ${proxy.host}:${proxy.port || location.port}`)
let clientDataStream

if (proxy && !connectOptions.viewerWsConnect && !isWebSocket) {
console.log(`using proxy ${proxy.host}:${proxy.port || location.port}`)
net['setProxy']({ hostname: proxy.host, port: proxy.port })
}

Expand All @@ -366,18 +370,22 @@ export async function connect (connectOptions: ConnectOptions) {
Object.assign(serverOptions, connectOptions.serverOverridesFlat ?? {})
setLoadingScreenStatus('Downloading minecraft data')
await Promise.all([
window._LOAD_MC_DATA(), // download mc data before we can use minecraft-data at all
downloadAllMinecraftData(), // download mc data before we can use minecraft-data at all
downloadOtherGameData()
])
setLoadingScreenStatus(loggingInMsg)
let dataDownloaded = false
const downloadMcData = async (version: string) => {
if (dataDownloaded) return
dataDownloaded = true
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 downloadMcDataOnConnect(version)
try {
// TODO! reload only after login packet (delay viewer display) so no unecessary reload after server one is isntalled
await resourcepackReload(version)
} catch (err) {
console.error(err)
Expand All @@ -386,8 +394,10 @@ export async function connect (connectOptions: ConnectOptions) {
throw err
}
}
setLoadingScreenStatus('Loading minecraft assets')
viewer.world.blockstatesModels = await import('mc-assets/dist/blockStatesModels.json')
void viewer.setVersion(version, options.useVersionsTextures === 'latest' ? version : options.useVersionsTextures)
miscUiState.loadedDataVersion = version
}

const downloadVersion = connectOptions.botVersion || (singleplayer ? serverOptions.version : undefined)
Expand Down Expand Up @@ -436,14 +446,18 @@ export async function connect (connectOptions: ConnectOptions) {
} 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)
const autoVersionSelect = await getServerInfo(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 = 'We have no idea what to do'
}
setLoadingScreenStatus(initialLoadingText)

if (isWebSocket) {
clientDataStream = (await getWebsocketStream(server.host!)).mineflayerStream
}

let newTokensCacheResult = null as any
const cachedTokens = typeof connectOptions.authenticatedAccount === 'object' ? connectOptions.authenticatedAccount.cachedTokens : {}
const authData = connectOptions.authenticatedAccount ? await microsoftAuthflow({
Expand All @@ -458,7 +472,6 @@ export async function connect (connectOptions: ConnectOptions) {
connectingServer: server.host
}) : undefined

let clientDataStream
if (p2pMultiplayer) {
clientDataStream = await connectToPeer(connectOptions.peerId!, connectOptions.peerOptions)
}
Expand All @@ -473,7 +486,7 @@ export async function connect (connectOptions: ConnectOptions) {
}

if (connectOptions.botVersion) {
miscUiState.loadedDataVersion = connectOptions.botVersion
await downloadMcData(connectOptions.botVersion)
}

bot = mineflayer.createBot({
Expand Down Expand Up @@ -562,7 +575,7 @@ export async function connect (connectOptions: ConnectOptions) {

bot.emit('inject_allowed')
bot._client.emit('connect')
} else if (connectOptions.viewerWsConnect) {
} else if (clientDataStream) {
// bot.emit('inject_allowed')
bot._client.emit('connect')
} else {
Expand Down
19 changes: 18 additions & 1 deletion src/mineflayer/mc-protocol.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Client } from 'minecraft-protocol'
import { appQueryParams } from '../appParams'
import { validatePacket } from './minecraft-protocol-extra'
import { downloadAllMinecraftData, getVersionAutoSelect } from '../connect'
import { pingServerVersion, validatePacket } from './minecraft-protocol-extra'
import { getWebsocketStream } from './websocket-core'

customEvents.on('mineflayerBotCreated', () => {
// todo move more code here
Expand All @@ -13,3 +15,18 @@ customEvents.on('mineflayerBotCreated', () => {
})
}
})


export const getServerInfo = async (ip: string, port?: number, preferredVersion = getVersionAutoSelect(), ping = false) => {
await downloadAllMinecraftData()
const isWebSocket = ip.startsWith('ws://') || ip.startsWith('wss://')
let stream
if (isWebSocket) {
stream = (await getWebsocketStream(ip)).mineflayerStream
}
return pingServerVersion(ip, port, {
...(stream ? { stream } : {}),
...(ping ? { noPongTimeout: 3000 } : {}),
...(preferredVersion ? { version: preferredVersion } : {}),
})
}
19 changes: 11 additions & 8 deletions src/mineflayer/minecraft-protocol-extra.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import EventEmitter from 'events'
import clientAutoVersion from 'minecraft-protocol/src/client/autoVersion'

export const pingServerVersion = async (ip: string, port?: number, preferredVersion?: string) => {
export const pingServerVersion = async (ip: string, port?: number, mergeOptions: Record<string, any> = {}) => {
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
noPongTimeout: Infinity, // disable timeout
...mergeOptions,
}
// let latency = 0
// fakeClient.autoVersionHooks = [(res) => {
// latency = res.latency
// }]
let latency = 0
let fullInfo = null
fakeClient.autoVersionHooks = [(res) => {
latency = res.latency
fullInfo = res
}]

// TODO! use client.socket.destroy() instead of client.end() for faster cleanup
await clientAutoVersion(fakeClient, options)
Expand All @@ -25,7 +27,8 @@ export const pingServerVersion = async (ip: string, port?: number, preferredVers
})
return {
version: fakeClient.version,
// latency,
latency,
fullInfo,
}
}

Expand Down
52 changes: 52 additions & 0 deletions src/mineflayer/websocket-core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Duplex } from 'stream'

class CustomDuplex extends Duplex {
constructor (options, public writeAction) {
super(options)
}

override _read () {}

override _write (chunk, encoding, callback) {
this.writeAction(chunk)
callback()
}
}

export const getWebsocketStream = async (host: string) => {
host = host.replace('ws://', '').replace('wss://', '')
const ws = new WebSocket(`ws://${host}`)
const clientDuplex = new CustomDuplex(undefined, data => {
ws.send(data)
})

ws.addEventListener('message', async message => {
let { data } = message
if (data instanceof Blob) {
data = await data.arrayBuffer()
}
clientDuplex.push(Buffer.from(data))
})

ws.addEventListener('close', () => {
console.log('ws closed')
clientDuplex.end()
})

ws.addEventListener('error', err => {
console.log('ws error', err)
})

await new Promise((resolve, reject) => {
ws.addEventListener('open', resolve)
ws.addEventListener('error', err => {
console.log('ws error', err)
reject(err)
})
})

return {
mineflayerStream: clientDuplex,
ws,
}
}
8 changes: 7 additions & 1 deletion src/react/AddServerOrConnect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,13 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
/>
</div>

<InputWithLabel label="Proxy Override" value={proxyOverride} disabled={lockConnect && (qsParamProxy !== null || !!placeholders?.proxyOverride)} onChange={({ target: { value } }) => setProxyOverride(value)} placeholder={placeholders?.proxyOverride} />
<InputWithLabel
label="Proxy Override"
value={proxyOverride}
disabled={lockConnect && (qsParamProxy !== null || !!placeholders?.proxyOverride) || serverIp.startsWith('ws://') || serverIp.startsWith('wss://')}
onChange={({ target: { value } }) => setProxyOverride(value)}
placeholder={serverIp.startsWith('ws://') || serverIp.startsWith('wss://') ? 'Not needed for websocket servers' : placeholders?.proxyOverride}
/>
<InputWithLabel
label="Username Override"
value={usernameOverride}
Expand Down
2 changes: 1 addition & 1 deletion src/react/AppStatusProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { activeModalStack, activeModalStacks, hideModal, insertActiveModalStack,
import { resetLocalStorageWorld } from '../browserfs'
import { fsState } from '../loadSave'
import { guessProblem } from '../errorLoadingScreenHelpers'
import { ConnectOptions } from '../connect'
import type { ConnectOptions } from '../connect'
import { downloadPacketsReplay, packetsReplaceSessionState, replayLogger } from '../packetsReplay'
import { getProxyDetails } from '../microsoftAuthflow'
import AppStatus from './AppStatus'
Expand Down
13 changes: 12 additions & 1 deletion src/react/GameInteractionOverlay.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { useRef } from 'react'
import { useSnapshot } from 'valtio'
import { subscribe, useSnapshot } from 'valtio'
import { useUtilsEffect } from '@zardoy/react-util'
import { options } from '../optionsStorage'
import { activeModalStack, isGameActive, miscUiState } from '../globalState'
import worldInteractions from '../worldInteractions'
import { onCameraMove, CameraMoveEvent } from '../cameraRotationControls'
import { pointerLock } from '../utils'
import { handleMovementStickDelta, joystickPointer } from './TouchAreasControls'

/** after what time of holding the finger start breaking the block */
Expand Down Expand Up @@ -185,3 +186,13 @@ export default function GameInteractionOverlay ({ zIndex }: { zIndex: number })
if (modalStack.length > 0 || !currentTouch) return null
return <GameInteractionOverlayInner zIndex={zIndex} />
}

subscribe(activeModalStack, () => {
if (activeModalStack.length === 0) {
if (isGameActive(false)) {
void pointerLock.requestPointerLock()
}
} else {
document.exitPointerLock?.()
}
})
17 changes: 15 additions & 2 deletions src/react/ServersListProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { useEffect, useMemo, useState } from 'react'
import { useUtilsEffect } from '@zardoy/react-util'
import { useSnapshot } from 'valtio'
import { ConnectOptions } from '../connect'
import { ConnectOptions, downloadAllMinecraftData, getVersionAutoSelect } from '../connect'
import { activeModalStack, hideCurrentModal, miscUiState, showModal } from '../globalState'
import supportedVersions from '../supportedVersions.mjs'
import { appQueryParams } from '../appParams'
import { fetchServerStatus, isServerValid } from '../api/mcStatusApi'
import { pingServerVersion } from '../mineflayer/minecraft-protocol-extra'
import { getServerInfo } from '../mineflayer/mc-protocol'
import ServersList from './ServersList'
import AddServerOrConnect, { BaseServerInfo } from './AddServerOrConnect'
import { useDidUpdateEffect } from './utils'
Expand Down Expand Up @@ -207,7 +209,18 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
try {
lastRequestStart = Date.now()
if (signal.aborted) return
const data = await fetchServerStatus(server.ip/* , signal */) // DONT ADD SIGNAL IT WILL CRUSH JS RUNTIME
const isWebSocket = server.ip.startsWith('ws://') || server.ip.startsWith('wss://')
let data
if (isWebSocket) {
const pingResult = await getServerInfo(server.ip, undefined, undefined, true)
data = {
formattedText: `${pingResult.version} server with a direct websocket connection`,
textNameRight: `ws ${pingResult.latency}ms`,
offline: false
}
} else {
data = await fetchServerStatus(server.ip/* , signal */) // DONT ADD SIGNAL IT WILL CRUSH JS RUNTIME
}
if (data) {
setAdditionalData(old => ({
...old,
Expand Down
Loading

0 comments on commit b5a16d5

Please sign in to comment.