Compare commits
91 commits
next
...
protocol-w
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
139ee9ad1f | ||
|
|
cd84acbe7a | ||
|
|
1fe6239b44 | ||
|
|
d9f1efda10 | ||
|
|
475d990217 | ||
|
|
4a949d3c14 |
||
|
|
ccd1130282 |
||
|
|
df2a337af5 | ||
|
|
92d444af62 | ||
|
|
b50215b1fa | ||
|
|
980779ce68 | ||
|
|
4483b73adf | ||
|
|
316fdd87af | ||
|
|
bdcf3d62fe | ||
|
|
078744000e | ||
|
|
290e64150c | ||
|
|
07e9725b2d | ||
|
|
bb6faf9b24 | ||
|
|
0839889d21 | ||
|
|
07e7c6f227 | ||
|
|
bc9ca189cd | ||
|
|
b9ca0573ac | ||
|
|
e743a039a3 | ||
|
|
b47258301c | ||
|
|
0648d55edd | ||
|
|
e7c240694f | ||
|
|
14e20a2cf6 | ||
|
|
9057d3acb5 | ||
|
|
547658f489 | ||
|
|
bf9c47dd26 | ||
|
|
9cede6dbbc | ||
|
|
6a5ac4f8d2 | ||
|
|
2630a57d35 | ||
|
|
20569747ca | ||
|
|
9888bd55c1 | ||
|
|
b501893ab2 | ||
|
|
e2b78333a1 | ||
|
|
e917764b76 | ||
|
|
ed041972c4 | ||
|
|
b579ee1767 | ||
|
|
d450a31547 | ||
|
|
853e0e1d84 | ||
|
|
8ddac97414 | ||
|
|
5eedb3c456 | ||
|
|
11abbfcbb1 | ||
|
|
8ee4dc37e7 | ||
|
|
dc2ad7ccce | ||
|
|
b483923009 | ||
|
|
82d0638eb8 | ||
|
|
cae2b612ba | ||
|
|
f88e9c8b61 | ||
|
|
136b051695 | ||
|
|
9fedafe776 | ||
|
|
ccb00043cf | ||
|
|
c1a7765fcb | ||
|
|
de3eddad89 | ||
|
|
e851f4fac2 | ||
|
|
f2f1c2538e | ||
|
|
5364085030 | ||
|
|
400f5982be | ||
|
|
dc073cd559 | ||
|
|
6eb50cde24 | ||
|
|
67d90a56fb | ||
|
|
f2307632a2 | ||
|
|
d74d860726 | ||
|
|
4d4637f710 | ||
|
|
847314d50f | ||
|
|
8a3c84745d | ||
|
|
3a9e2aa384 | ||
|
|
7cc562bd02 | ||
|
|
0597a3dad2 | ||
|
|
cefdf5362f | ||
|
|
d197859d47 | ||
|
|
1861edf567 | ||
|
|
d8294d565b | ||
|
|
4381ef4f75 | ||
|
|
c65db9a8cb | ||
|
|
34972e4e71 | ||
|
|
638dd6711e |
||
|
|
9356daaefc |
||
|
|
fb10179691 | ||
|
|
7e74633c14 | ||
|
|
2d7ec12a75 | ||
|
|
c626d105ff | ||
|
|
8db6b5bb51 | ||
|
|
2848ab63d3 | ||
|
|
537658476d | ||
|
|
044153c2dc | ||
|
|
380c21486b | ||
|
|
aed5b40516 | ||
|
|
3051cc35f5 |
17 changed files with 704 additions and 224 deletions
|
|
@ -199,7 +199,8 @@
|
|||
"no-bitwise": "off",
|
||||
"unicorn/filename-case": "off",
|
||||
"max-depth": "off",
|
||||
"unicorn/no-typeof-undefined": "off"
|
||||
"unicorn/no-typeof-undefined": "off",
|
||||
"unicorn/relative-url-style": "off"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -61,6 +61,16 @@ export const appAndRendererSharedConfig = () => defineConfig({
|
|||
],
|
||||
tools: {
|
||||
rspack (config, helpers) {
|
||||
if (process.env.SINGLE_FILE_BUILD === 'true') {
|
||||
config.module.rules.push({
|
||||
test: /\.worker\.(js|ts)$/,
|
||||
loader: "worker-rspack-loader",
|
||||
options: {
|
||||
inline: "no-fallback",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8'))
|
||||
const hasFileProtocol = Object.values(packageJson.pnpm.overrides).some((dep) => (dep as string).startsWith('file:'))
|
||||
if (hasFileProtocol) {
|
||||
|
|
|
|||
|
|
@ -33,3 +33,9 @@ export function sectionPos (pos: { x: number, y: number, z: number }) {
|
|||
const z = Math.floor(pos.z / 16)
|
||||
return [x, y, z]
|
||||
}
|
||||
// doesn't support snapshots
|
||||
|
||||
export const toMajorVersion = version => {
|
||||
const [a, b] = (String(version)).split('.')
|
||||
return `${a}.${b}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,18 @@
|
|||
export function createWorkerProxy<T extends Record<string, (...args: any[]) => void>> (handlers: T, channel?: MessagePort): { __workerProxy: T } {
|
||||
export function createWorkerProxy<T extends Record<string, (...args: any[]) => void | Promise<any>>> (handlers: T, channel?: MessagePort): { __workerProxy: T } {
|
||||
const target = channel ?? globalThis
|
||||
target.addEventListener('message', (event: any) => {
|
||||
const { type, args } = event.data
|
||||
const { type, args, msgId } = event.data
|
||||
if (handlers[type]) {
|
||||
handlers[type](...args)
|
||||
const result = handlers[type](...args)
|
||||
if (result instanceof Promise) {
|
||||
void result.then((result) => {
|
||||
target.postMessage({
|
||||
type: 'result',
|
||||
msgId,
|
||||
args: [result]
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
return null as any
|
||||
|
|
@ -23,6 +32,7 @@ export function createWorkerProxy<T extends Record<string, (...args: any[]) => v
|
|||
export const useWorkerProxy = <T extends { __workerProxy: Record<string, (...args: any[]) => void> }> (worker: Worker | MessagePort, autoTransfer = true): T['__workerProxy'] & {
|
||||
transfer: (...args: Transferable[]) => T['__workerProxy']
|
||||
} => {
|
||||
let messageId = 0
|
||||
// in main thread
|
||||
return new Proxy({} as any, {
|
||||
get (target, prop) {
|
||||
|
|
@ -41,11 +51,25 @@ export const useWorkerProxy = <T extends { __workerProxy: Record<string, (...arg
|
|||
}
|
||||
}
|
||||
return (...args: any[]) => {
|
||||
const msgId = messageId++
|
||||
const transfer = autoTransfer ? args.filter(arg => arg instanceof ArrayBuffer || arg instanceof MessagePort || arg instanceof ImageBitmap || arg instanceof OffscreenCanvas || arg instanceof ImageData) : []
|
||||
worker.postMessage({
|
||||
type: prop,
|
||||
msgId,
|
||||
args,
|
||||
}, transfer as any[])
|
||||
return {
|
||||
// eslint-disable-next-line unicorn/no-thenable
|
||||
then (onfulfilled: (value: any) => void) {
|
||||
const handler = ({ data }: MessageEvent): void => {
|
||||
if (data.type === 'result' && data.msgId === msgId) {
|
||||
onfulfilled(data.args[0])
|
||||
worker.removeEventListener('message', handler as EventListener)
|
||||
}
|
||||
}
|
||||
worker.addEventListener('message', handler as EventListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -9,13 +9,12 @@ import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
|
|||
import { generateSpiralMatrix } from 'flying-squid/dist/utils'
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
import { dynamicMcDataFiles } from '../../buildMesherConfig.mjs'
|
||||
import { toMajorVersion } from '../../../src/utils'
|
||||
import { ResourcesManager } from '../../../src/resourcesManager'
|
||||
import { DisplayWorldOptions, GraphicsInitOptions, RendererReactiveState } from '../../../src/appViewer'
|
||||
import { SoundSystem } from '../three/threeJsSound'
|
||||
import { buildCleanupDecorator } from './cleanupDecorator'
|
||||
import { HighestBlockInfo, MesherGeometryOutput, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig } from './mesher/shared'
|
||||
import { chunkPos } from './simpleUtils'
|
||||
import { chunkPos, toMajorVersion } from './simpleUtils'
|
||||
import { addNewStat, removeAllStats, removeStat, updatePanesVisibility, updateStatText } from './ui/newStats'
|
||||
import { WorldDataEmitter } from './worldDataEmitter'
|
||||
import { IPlayerState } from './basePlayerState'
|
||||
|
|
|
|||
|
|
@ -208,6 +208,13 @@ const appConfig = defineConfig({
|
|||
if (singleBuildFiles.length !== 1 || singleBuildFiles[0] !== 'index.html') {
|
||||
throw new Error('Single file build must only have index.html in the dist/single folder. Ensure workers are imported & built correctly.')
|
||||
}
|
||||
// check if dist/static/js/async is empty
|
||||
if (fs.existsSync('./dist/static/js/async')) {
|
||||
const asyncFiles = fs.readdirSync('./dist/static/js/async')
|
||||
if (asyncFiles.length > 0) {
|
||||
throw new Error('dist/static/js/async must be empty. Ensure workers are imported & built correctly.')
|
||||
}
|
||||
}
|
||||
|
||||
// process index.html
|
||||
const singleBuildHtml = './dist/single/index.html'
|
||||
|
|
@ -224,8 +231,9 @@ const appConfig = defineConfig({
|
|||
// write output file size
|
||||
console.log('single file size', (fs.statSync(singleBuildHtml).size / 1024 / 1024).toFixed(2), 'mb')
|
||||
} else {
|
||||
patchWorkerImport()
|
||||
if (!disableServiceWorker) {
|
||||
const { count, size, warnings } = await generateSW({
|
||||
const { count, size, warnings } = await generateSW({
|
||||
// dontCacheBustURLsMatching: [new RegExp('...')],
|
||||
globDirectory: 'dist',
|
||||
skipWaiting: true,
|
||||
|
|
@ -254,3 +262,21 @@ export default mergeRsbuildConfig(
|
|||
appAndRendererSharedConfig(),
|
||||
appConfig
|
||||
)
|
||||
|
||||
const patchWorkerImport = () => {
|
||||
const workerFiles = fs.readdirSync('./dist/static/js/async').filter(x => x.endsWith('.js'))
|
||||
let patched = false
|
||||
for (const file of workerFiles) {
|
||||
const filePath = `./dist/static/js/async/${file}`
|
||||
const content = fs.readFileSync(filePath, 'utf8')
|
||||
const matches = content.match(/importScripts\([^)]+\)/g) || []
|
||||
if (matches.length > 1) throw new Error('Multiple importScripts found in ' + filePath)
|
||||
const newContent = content.replace(/importScripts\(\w+\.\w+/,
|
||||
"importScripts(location.pathname.split('/').slice(0, -4).join('/')+'/'")
|
||||
if (newContent !== content) {
|
||||
fs.writeFileSync(filePath, newContent, 'utf8')
|
||||
patched = true
|
||||
}
|
||||
}
|
||||
if (!patched) throw new Error('No importScripts found in any worker files')
|
||||
}
|
||||
|
|
|
|||
151
src/index.ts
151
src/index.ts
|
|
@ -17,8 +17,6 @@ import './mineflayer/timers'
|
|||
import { getServerInfo } from './mineflayer/mc-protocol'
|
||||
import { onGameLoad } from './inventoryWindows'
|
||||
import initCollisionShapes from './getCollisionInteractionShapes'
|
||||
import protocolMicrosoftAuth from 'minecraft-protocol/src/client/microsoftAuth'
|
||||
import microsoftAuthflow from './microsoftAuthflow'
|
||||
import { Duplex } from 'stream'
|
||||
|
||||
import './scaleInterface'
|
||||
|
|
@ -75,7 +73,7 @@ import { saveToBrowserMemory } from './react/PauseScreen'
|
|||
import './devReload'
|
||||
import './water'
|
||||
import { ConnectOptions, loadMinecraftData, getVersionAutoSelect, downloadOtherGameData, downloadAllMinecraftData } from './connect'
|
||||
import { ref, subscribe } from 'valtio'
|
||||
import { subscribe } from 'valtio'
|
||||
import { signInMessageState } from './react/SignInMessageProvider'
|
||||
import { updateAuthenticatedAccountData, updateLoadedServerData, updateServerConnectionHistory } from './react/serversStorage'
|
||||
import packetsPatcher from './mineflayer/plugins/packetsPatcher'
|
||||
|
|
@ -97,6 +95,7 @@ import { createConsoleLogProgressReporter, createFullScreenProgressReporter, Pro
|
|||
import { appViewer } from './appViewer'
|
||||
import './appViewerLoad'
|
||||
import { registerOpenBenchmarkListener } from './benchmark'
|
||||
import { getProtocolClientGetter } from './protocolWorker/protocolMain'
|
||||
import { tryHandleBuiltinCommand } from './builtinCommands'
|
||||
|
||||
window.debug = debug
|
||||
|
|
@ -162,11 +161,6 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
}
|
||||
})
|
||||
}
|
||||
if (sessionStorage.delayLoadUntilClick) {
|
||||
await new Promise(resolve => {
|
||||
window.addEventListener('click', resolve)
|
||||
})
|
||||
}
|
||||
|
||||
miscUiState.hasErrors = false
|
||||
lastConnectOptions.value = connectOptions
|
||||
|
|
@ -184,8 +178,8 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
|
||||
const { renderDistance: renderDistanceSingleplayer, multiplayerRenderDistance } = options
|
||||
|
||||
const parsedServer = parseServerAddress(connectOptions.server)
|
||||
const server = { host: parsedServer.host, port: parsedServer.port }
|
||||
const serverParsed = parseServerAddress(connectOptions.server)
|
||||
const server = { host: serverParsed.host, port: serverParsed.port }
|
||||
if (connectOptions.proxy?.startsWith(':')) {
|
||||
connectOptions.proxy = `${location.protocol}//${location.hostname}${connectOptions.proxy}`
|
||||
}
|
||||
|
|
@ -193,12 +187,12 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
const https = connectOptions.proxy.startsWith('https://') || location.protocol === 'https:'
|
||||
connectOptions.proxy = `${connectOptions.proxy}:${https ? 443 : 80}`
|
||||
}
|
||||
const parsedProxy = parseServerAddress(connectOptions.proxy, false)
|
||||
const proxy = { host: parsedProxy.host, port: parsedProxy.port }
|
||||
const proxyParsed = parseServerAddress(connectOptions.proxy, false)
|
||||
const proxy = { host: proxyParsed.host, port: proxyParsed.port }
|
||||
let { username } = connectOptions
|
||||
|
||||
if (connectOptions.server) {
|
||||
console.log(`connecting to ${server.host}:${server.port ?? 25_565}`)
|
||||
console.log(`connecting to ${serverParsed.serverIpFull}`)
|
||||
}
|
||||
console.log('using player username', username)
|
||||
|
||||
|
|
@ -242,14 +236,13 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
})
|
||||
}
|
||||
}
|
||||
let lastPacket = undefined as string | undefined
|
||||
const lastPacket = undefined as string | undefined
|
||||
const onPossibleErrorDisconnect = () => {
|
||||
if (lastPacket && bot?._client && bot._client.state !== states.PLAY) {
|
||||
appStatusState.descriptionHint = `Last Server Packet: ${lastPacket}`
|
||||
}
|
||||
}
|
||||
const handleError = (err) => {
|
||||
console.error(err)
|
||||
if (err === 'ResizeObserver loop completed with undelivered notifications.') {
|
||||
return
|
||||
}
|
||||
|
|
@ -287,16 +280,14 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
|
||||
let clientDataStream: Duplex | undefined
|
||||
|
||||
if (connectOptions.server && !connectOptions.viewerWsConnect && !parsedServer.isWebSocket) {
|
||||
if (connectOptions.server && !connectOptions.viewerWsConnect && !serverParsed.isWebSocket) {
|
||||
console.log(`using proxy ${proxy.host}:${proxy.port || location.port}`)
|
||||
net['setProxy']({ hostname: proxy.host, port: proxy.port, headers: { Authorization: `Bearer ${new URLSearchParams(location.search).get('token') ?? ''}` } })
|
||||
}
|
||||
|
||||
const renderDistance = singleplayer ? renderDistanceSingleplayer : multiplayerRenderDistance
|
||||
let updateDataAfterJoin = () => { }
|
||||
let localServer
|
||||
let localReplaySession: ReturnType<typeof startLocalReplayServer> | undefined
|
||||
let lastKnownKickReason = undefined as string | undefined
|
||||
try {
|
||||
const serverOptions = defaultsDeep({}, connectOptions.serverOverrides ?? {}, options.localServerOptions, defaultServerOptions)
|
||||
Object.assign(serverOptions, connectOptions.serverOverridesFlat ?? {})
|
||||
|
|
@ -415,30 +406,12 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
}
|
||||
setLoadingScreenStatus(initialLoadingText)
|
||||
|
||||
if (parsedServer.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({
|
||||
tokenCaches: cachedTokens,
|
||||
proxyBaseUrl: connectOptions.proxy,
|
||||
setProgressText (text) {
|
||||
setLoadingScreenStatus(text)
|
||||
},
|
||||
setCacheResult (result) {
|
||||
newTokensCacheResult = result
|
||||
},
|
||||
connectingServer: server.host
|
||||
}) : undefined
|
||||
|
||||
if (p2pMultiplayer) {
|
||||
clientDataStream = await connectToPeer(connectOptions.peerId!, connectOptions.peerOptions)
|
||||
}
|
||||
if (connectOptions.viewerWsConnect) {
|
||||
const { version, time, requiresPass } = await getViewerVersionData(connectOptions.viewerWsConnect)
|
||||
let password
|
||||
let password: string | null = null
|
||||
if (requiresPass) {
|
||||
password = prompt('Enter password')
|
||||
if (!password) {
|
||||
|
|
@ -463,6 +436,8 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
}
|
||||
|
||||
const brand = clientDataStream ? 'minecraft-web-client' : undefined
|
||||
const createClient = await getProtocolClientGetter(proxy, connectOptions, serverParsed)
|
||||
|
||||
bot = mineflayer.createBot({
|
||||
host: server.host,
|
||||
port: server.port ? +server.port : undefined,
|
||||
|
|
@ -483,53 +458,13 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
connect () { },
|
||||
Client: CustomChannelClient as any,
|
||||
} : {},
|
||||
onMsaCode (data) {
|
||||
signInMessageState.code = data.user_code
|
||||
signInMessageState.link = data.verification_uri
|
||||
signInMessageState.expiresOn = Date.now() + data.expires_in * 1000
|
||||
},
|
||||
sessionServer: authData?.sessionEndpoint?.toString(),
|
||||
auth: connectOptions.authenticatedAccount ? async (client, options) => {
|
||||
authData!.setOnMsaCodeCallback(options.onMsaCode)
|
||||
authData?.setConnectingVersion(client.version)
|
||||
//@ts-expect-error
|
||||
client.authflow = authData!.authFlow
|
||||
try {
|
||||
signInMessageState.abortController = ref(new AbortController())
|
||||
await Promise.race([
|
||||
protocolMicrosoftAuth.authenticate(client, options),
|
||||
new Promise((_r, reject) => {
|
||||
signInMessageState.abortController.signal.addEventListener('abort', () => {
|
||||
reject(new UserError('Aborted by user'))
|
||||
})
|
||||
})
|
||||
])
|
||||
if (signInMessageState.shouldSaveToken) {
|
||||
updateAuthenticatedAccountData(accounts => {
|
||||
const existingAccount = accounts.find(a => a.username === client.username)
|
||||
if (existingAccount) {
|
||||
existingAccount.cachedTokens = { ...existingAccount.cachedTokens, ...newTokensCacheResult }
|
||||
} else {
|
||||
accounts.push({
|
||||
username: client.username,
|
||||
cachedTokens: { ...cachedTokens, ...newTokensCacheResult }
|
||||
})
|
||||
}
|
||||
return accounts
|
||||
})
|
||||
updateDataAfterJoin = () => {
|
||||
updateLoadedServerData(s => ({ ...s, authenticatedAccountOverride: client.username }), connectOptions.serverIndex)
|
||||
}
|
||||
} else {
|
||||
updateDataAfterJoin = () => {
|
||||
updateLoadedServerData(s => ({ ...s, authenticatedAccountOverride: undefined }), connectOptions.serverIndex)
|
||||
}
|
||||
}
|
||||
setLoadingScreenStatus('Authentication successful. Logging in to server')
|
||||
} finally {
|
||||
signInMessageState.code = ''
|
||||
get client () {
|
||||
if (clientDataStream || singleplayer || p2pMultiplayer || localReplaySession || connectOptions.viewerWsConnect || (!options.protocolWorkerOptimisation && !serverParsed.isWebSocket)) {
|
||||
return undefined
|
||||
}
|
||||
} : undefined,
|
||||
return createClient.call(this)
|
||||
},
|
||||
// auth: connectOptions.authenticatedAccount ? : undefined,
|
||||
username,
|
||||
viewDistance: renderDistance,
|
||||
checkTimeoutInterval: 240 * 1000,
|
||||
|
|
@ -562,50 +497,6 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
} else if (clientDataStream) {
|
||||
// bot.emit('inject_allowed')
|
||||
bot._client.emit('connect')
|
||||
} else {
|
||||
const setupConnectHandlers = () => {
|
||||
Socket.prototype['handleStringMessage'] = function (message: string) {
|
||||
if (message.startsWith('proxy-message') || message.startsWith('proxy-command:')) { // for future
|
||||
return false
|
||||
}
|
||||
if (message.startsWith('proxy-shutdown:')) {
|
||||
lastKnownKickReason = message.slice('proxy-shutdown:'.length)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
bot._client.socket.on('connect', () => {
|
||||
console.log('Proxy WebSocket connection established')
|
||||
//@ts-expect-error
|
||||
bot._client.socket._ws.addEventListener('close', () => {
|
||||
console.log('WebSocket connection closed')
|
||||
setTimeout(() => {
|
||||
if (bot) {
|
||||
bot.emit('end', 'WebSocket connection closed with unknown reason')
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
bot._client.socket.on('close', () => {
|
||||
setTimeout(() => {
|
||||
if (bot) {
|
||||
bot.emit('end', 'WebSocket connection closed with unknown reason')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
// socket setup actually can be delayed because of dns lookup
|
||||
if (bot._client.socket) {
|
||||
setupConnectHandlers()
|
||||
} else {
|
||||
const originalSetSocket = bot._client.setSocket.bind(bot._client)
|
||||
bot._client.setSocket = (socket) => {
|
||||
if (!bot) return
|
||||
originalSetSocket(socket)
|
||||
setupConnectHandlers()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
|
|
@ -641,7 +532,7 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
})
|
||||
|
||||
const packetBeforePlay = (_, __, ___, fullBuffer) => {
|
||||
lastPacket = fullBuffer.toString()
|
||||
// lastPacket = fullBuffer.toString()
|
||||
}
|
||||
bot._client.on('packet', packetBeforePlay as any)
|
||||
const playStateSwitch = (newState) => {
|
||||
|
|
@ -654,9 +545,6 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
bot.on('end', (endReason) => {
|
||||
if (ended) return
|
||||
console.log('disconnected for', endReason)
|
||||
if (endReason === 'socketClosed') {
|
||||
endReason = lastKnownKickReason ?? 'Connection with proxy server lost'
|
||||
}
|
||||
// close all modals
|
||||
for (const modal of activeModalStack) {
|
||||
hideModal(modal)
|
||||
|
|
@ -732,7 +620,6 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
localStorage.removeItem('lastConnectOptions')
|
||||
}
|
||||
connectOptions.onSuccessfulPlay?.()
|
||||
updateDataAfterJoin()
|
||||
if (connectOptions.autoLoginPassword) {
|
||||
setTimeout(() => {
|
||||
bot.chat(`/login ${connectOptions.autoLoginPassword}`)
|
||||
|
|
|
|||
|
|
@ -7,16 +7,10 @@ import { getWebsocketStream } from './websocket-core'
|
|||
|
||||
let lastPacketTime = 0
|
||||
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)
|
||||
lastPacketTime = performance.now()
|
||||
});
|
||||
(bot._client as unknown as Client).on('writePacket', (name, params) => {
|
||||
validatePacket(name, params, Buffer.alloc(0), false)
|
||||
})
|
||||
}
|
||||
(bot._client as unknown as Client).on('packet', (data, packetMeta, buffer, fullBuffer) => {
|
||||
lastPacketTime = performance.now()
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
setInterval(() => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
import { versionToNumber } from 'renderer/viewer/common/utils'
|
||||
import { getProtocolWorkerChannel } from '../../protocolWorker/protocolMain'
|
||||
|
||||
export default () => {
|
||||
let i = 0
|
||||
bot.pingProxy = async () => {
|
||||
const curI = ++i
|
||||
if (bot && (!bot._client as any)._ws) {
|
||||
const result = await getProtocolWorkerChannel()?.pingProxy(curI)
|
||||
return result ?? -1
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
//@ts-expect-error
|
||||
bot._client.socket._ws.send(`ping:${curI}`)
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ const defaultOptions = {
|
|||
jeiEnabled: true as boolean | Array<'creative' | 'survival' | 'adventure' | 'spectator'>,
|
||||
preventBackgroundTimeoutKick: false,
|
||||
preventSleep: false,
|
||||
protocolWorkerOptimisation: true,
|
||||
debugContro: false,
|
||||
|
||||
// antiAliasing: false,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,10 @@
|
|||
import { ref } from 'valtio'
|
||||
import { signInMessageState } from '../react/SignInMessageProvider'
|
||||
import { updateAuthenticatedAccountData, updateLoadedServerData } from '../react/serversStorage'
|
||||
import { setLoadingScreenStatus } from '../appStatus'
|
||||
import { ConnectOptions } from '../connect'
|
||||
import { showNotification } from '../react/NotificationProvider'
|
||||
|
||||
export const getProxyDetails = async (proxyBaseUrl: string) => {
|
||||
if (!proxyBaseUrl.startsWith('http')) proxyBaseUrl = `${isPageSecure() ? 'https' : 'http'}://${proxyBaseUrl}`
|
||||
const url = `${proxyBaseUrl}/api/vm/net/connect`
|
||||
|
|
@ -10,13 +17,14 @@ export const getProxyDetails = async (proxyBaseUrl: string) => {
|
|||
return result
|
||||
}
|
||||
|
||||
export default async ({ tokenCaches, proxyBaseUrl, setProgressText = (text) => { }, setCacheResult, connectingServer }) => {
|
||||
export const getAuthData = async ({ tokenCaches, proxyBaseUrl, setProgressText = (text) => { }, connectingServer }) => {
|
||||
let onMsaCodeCallback
|
||||
let connectingVersion = ''
|
||||
// const authEndpoint = 'http://localhost:3000/'
|
||||
// const sessionEndpoint = 'http://localhost:3000/session'
|
||||
let authEndpoint: URL | undefined
|
||||
let sessionEndpoint: URL | undefined
|
||||
let newTokensCacheResult = null as any
|
||||
const result = await getProxyDetails(proxyBaseUrl)
|
||||
|
||||
try {
|
||||
|
|
@ -32,7 +40,7 @@ export default async ({ tokenCaches, proxyBaseUrl, setProgressText = (text) => {
|
|||
async getMinecraftJavaToken () {
|
||||
setProgressText('Authenticating with Microsoft account')
|
||||
if (!window.crypto && !isPageSecure()) throw new Error('Crypto API is available only in secure contexts. Be sure to use https!')
|
||||
let result = null
|
||||
let result = null as any
|
||||
await fetch(authEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
|
@ -73,7 +81,7 @@ export default async ({ tokenCaches, proxyBaseUrl, setProgressText = (text) => {
|
|||
}
|
||||
if (json.error) throw new Error(json.error)
|
||||
if (json.token) result = json
|
||||
if (json.newCache) setCacheResult(json.newCache)
|
||||
if (json.newCache) newTokensCacheResult = json.newCache
|
||||
}
|
||||
|
||||
const strings = decoder.decode(value)
|
||||
|
|
@ -86,11 +94,7 @@ export default async ({ tokenCaches, proxyBaseUrl, setProgressText = (text) => {
|
|||
}
|
||||
return reader.read().then(processText)
|
||||
})
|
||||
const restoredData = await restoreData(result)
|
||||
if (restoredData?.certificates?.profileKeys?.privatePEM) {
|
||||
restoredData.certificates.profileKeys.private = restoredData.certificates.profileKeys.privatePEM
|
||||
}
|
||||
return restoredData
|
||||
return result
|
||||
}
|
||||
}
|
||||
return {
|
||||
|
|
@ -101,77 +105,68 @@ export default async ({ tokenCaches, proxyBaseUrl, setProgressText = (text) => {
|
|||
},
|
||||
setConnectingVersion (version) {
|
||||
connectingVersion = version
|
||||
},
|
||||
get newTokensCacheResult () {
|
||||
return newTokensCacheResult
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const authFlowMainThread = async (worker: Worker, authData: Awaited<ReturnType<typeof getAuthData>>, connectOptions: ConnectOptions, setActionAfterJoin: (action: () => void) => void) => {
|
||||
const cachedTokens = typeof connectOptions.authenticatedAccount === 'object' ? connectOptions.authenticatedAccount.cachedTokens : {}
|
||||
signInMessageState.abortController = ref(new AbortController())
|
||||
await new Promise<void>(resolve => {
|
||||
worker.addEventListener('message', ({ data }) => {
|
||||
if (data.type === 'authFlow') {
|
||||
authData.setConnectingVersion(data.version)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
authData.setOnMsaCodeCallback((codeData) => {
|
||||
signInMessageState.code = codeData.user_code
|
||||
signInMessageState.link = codeData.verification_uri
|
||||
signInMessageState.expiresOn = Date.now() + codeData.expires_in * 1000
|
||||
})
|
||||
|
||||
const data = await authData.authFlow.getMinecraftJavaToken()
|
||||
signInMessageState.code = ''
|
||||
if (!data) return
|
||||
const username = data.profile.name
|
||||
if (signInMessageState.shouldSaveToken) {
|
||||
updateAuthenticatedAccountData(accounts => {
|
||||
const existingAccount = accounts.find(a => a.username === username)
|
||||
if (existingAccount) {
|
||||
existingAccount.cachedTokens = { ...existingAccount.cachedTokens, ...authData.newTokensCacheResult }
|
||||
} else {
|
||||
accounts.push({
|
||||
username,
|
||||
cachedTokens: { ...cachedTokens, ...authData.newTokensCacheResult }
|
||||
})
|
||||
}
|
||||
showNotification(`Account ${username} saved`)
|
||||
return accounts
|
||||
})
|
||||
setActionAfterJoin(() => {
|
||||
updateLoadedServerData(s => ({ ...s, authenticatedAccountOverride: username }), connectOptions.serverIndex)
|
||||
})
|
||||
} else {
|
||||
setActionAfterJoin(() => {
|
||||
updateLoadedServerData(s => ({ ...s, authenticatedAccountOverride: undefined }), connectOptions.serverIndex)
|
||||
})
|
||||
}
|
||||
worker.postMessage({
|
||||
type: 'authflowResult',
|
||||
data
|
||||
})
|
||||
setLoadingScreenStatus('Authentication successful. Logging in to server')
|
||||
}
|
||||
|
||||
function isPageSecure (url = window.location.href) {
|
||||
return !url.startsWith('http:')
|
||||
}
|
||||
|
||||
// restore dates from strings
|
||||
const restoreData = async (json) => {
|
||||
const promises = [] as Array<Promise<void>>
|
||||
if (typeof json === 'object' && json) {
|
||||
for (const [key, value] of Object.entries(json)) {
|
||||
if (typeof value === 'string') {
|
||||
promises.push(tryRestorePublicKey(value, key, json))
|
||||
if (value.endsWith('Z')) {
|
||||
const date = new Date(value)
|
||||
if (!isNaN(date.getTime())) {
|
||||
json[key] = date
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await restoreData(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
return json
|
||||
}
|
||||
|
||||
const tryRestorePublicKey = async (value: string, name: string, parent: { [x: string]: any }) => {
|
||||
value = value.trim()
|
||||
if (!name.endsWith('PEM') || !value.startsWith('-----BEGIN RSA PUBLIC KEY-----') || !value.endsWith('-----END RSA PUBLIC KEY-----')) return
|
||||
const der = pemToArrayBuffer(value)
|
||||
const key = await window.crypto.subtle.importKey(
|
||||
'spki', // Specify that the data is in SPKI format
|
||||
der,
|
||||
{
|
||||
name: 'RSA-OAEP',
|
||||
hash: { name: 'SHA-256' }
|
||||
},
|
||||
true,
|
||||
['encrypt'] // Specify key usages
|
||||
)
|
||||
const originalName = name.replace('PEM', '')
|
||||
const exported = await window.crypto.subtle.exportKey('spki', key)
|
||||
const exportedBuffer = new Uint8Array(exported)
|
||||
parent[originalName] = {
|
||||
export () {
|
||||
return exportedBuffer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pemToArrayBuffer (pem) {
|
||||
// Fetch the part of the PEM string between header and footer
|
||||
const pemHeader = '-----BEGIN RSA PUBLIC KEY-----'
|
||||
const pemFooter = '-----END RSA PUBLIC KEY-----'
|
||||
const pemContents = pem.slice(pemHeader.length, pem.length - pemFooter.length).trim()
|
||||
const binaryDerString = atob(pemContents.replaceAll(/\s/g, ''))
|
||||
const binaryDer = new Uint8Array(binaryDerString.length)
|
||||
for (let i = 0; i < binaryDerString.length; i++) {
|
||||
binaryDer[i] = binaryDerString.codePointAt(i)!
|
||||
}
|
||||
return binaryDer.buffer
|
||||
}
|
||||
|
||||
const urlWithBase = (url: string, base: string) => {
|
||||
const defaultBase = isPageSecure() ? 'https' : 'http'
|
||||
if (!base.startsWith('http')) base = `${defaultBase}://${base}`
|
||||
320
src/protocolWorker/protocol.worker.ts
Normal file
320
src/protocolWorker/protocol.worker.ts
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
/* eslint-disable no-restricted-globals */
|
||||
import './protocolWorkerGlobals'
|
||||
import * as net from 'net'
|
||||
import EventEmitter from 'events'
|
||||
import { Duplex } from 'stream'
|
||||
import { Client, createClient } from 'minecraft-protocol'
|
||||
import protocolMicrosoftAuth from 'minecraft-protocol/src/client/microsoftAuth'
|
||||
import { createWorkerProxy } from 'renderer/viewer/lib/workerProxy'
|
||||
import { validatePacket } from '../mineflayer/minecraft-protocol-extra'
|
||||
import { getWebsocketStream } from '../mineflayer/websocket-core'
|
||||
|
||||
// This is a Web Worker for handling minecraft connection: protocol packet serialization/deserialization
|
||||
|
||||
// TODO: use another strategy by sending all events instead
|
||||
const REDIRECT_EVENTS = ['connection', 'listening', 'playerJoin', 'connect_allowed', 'connect']
|
||||
const REIDRECT_EVENTS_WITH_ARGS = ['end', 'playerChat', 'systemChat', 'state']
|
||||
const ENABLE_TRANSFER = false
|
||||
|
||||
const emitEvent = (event: string, ...args: any[]) => {
|
||||
const transfer = ENABLE_TRANSFER ? args.filter(arg => arg instanceof ArrayBuffer || arg instanceof MessagePort || arg instanceof ImageBitmap || arg instanceof OffscreenCanvas || arg instanceof ImageData) : []
|
||||
self.postMessage({ type: 'event', event, args }, transfer as any)
|
||||
}
|
||||
let client: Client
|
||||
const registeredChannels = [] as string[]
|
||||
let skipWriteLog = false
|
||||
|
||||
type ProtocolWorkerInitOptions = {
|
||||
options: any
|
||||
noPacketsValidation: boolean
|
||||
useAuthFlow: boolean
|
||||
isWebSocket: boolean
|
||||
}
|
||||
|
||||
let clientCreationPromise: Promise<void> | undefined
|
||||
let lastKnownKickReason: string | undefined
|
||||
export const PROXY_WORKER_TYPE = createWorkerProxy({
|
||||
setProxy (data: { hostname: string, port: number | undefined }) {
|
||||
console.log('[protocolWorker] using proxy', data)
|
||||
net['setProxy']({
|
||||
hostname: data.hostname,
|
||||
port: data.port
|
||||
})
|
||||
},
|
||||
async init ({ options, noPacketsValidation, useAuthFlow, isWebSocket }: ProtocolWorkerInitOptions) {
|
||||
if (client) throw new Error('Client already initialized')
|
||||
const withResolvers = Promise.withResolvers<void>()
|
||||
clientCreationPromise = withResolvers.promise
|
||||
|
||||
// let stream: Duplex | undefined
|
||||
if (isWebSocket) {
|
||||
options.stream = (await getWebsocketStream(options.host)).mineflayerStream
|
||||
}
|
||||
|
||||
await globalThis._LOAD_MC_DATA()
|
||||
if (useAuthFlow) {
|
||||
options.auth = authFlowWorkerThread
|
||||
}
|
||||
client = createClient(options)
|
||||
|
||||
for (const event of REDIRECT_EVENTS) {
|
||||
client.on(event, () => {
|
||||
emitEvent(event)
|
||||
})
|
||||
}
|
||||
|
||||
for (const event of REIDRECT_EVENTS_WITH_ARGS) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
client.on(event, (...args) => {
|
||||
if (event === 'end') {
|
||||
if (args[0] === 'socketClosed') {
|
||||
args[0] = lastKnownKickReason || 'Connection with proxy server has been lost'
|
||||
}
|
||||
}
|
||||
emitEvent(event, ...args)
|
||||
})
|
||||
}
|
||||
|
||||
const oldWrite = client.write
|
||||
client.write = (...args) => {
|
||||
if (!skipWriteLog) {
|
||||
emitEvent('writePacket', ...args)
|
||||
}
|
||||
return oldWrite.apply(client, args)
|
||||
}
|
||||
|
||||
client.on('packet', (data, packetMeta, buffer, fullBuffer) => {
|
||||
if (window.stopPacketsProcessing) return
|
||||
if (!noPacketsValidation) {
|
||||
validatePacket(packetMeta.name, data, fullBuffer, true)
|
||||
}
|
||||
emitEvent('packet', data, packetMeta, {}, { byteLength: fullBuffer.byteLength })
|
||||
})
|
||||
|
||||
if (isWebSocket) {
|
||||
client.emit('connect')
|
||||
}
|
||||
|
||||
wrapClientSocket(client)
|
||||
setupPropertiesSync(client)
|
||||
withResolvers.resolve()
|
||||
debugAnalyzeNeededProperties(client)
|
||||
clientCreationPromise = undefined
|
||||
},
|
||||
call (data: { name: string, args: any[] }) {
|
||||
// ignore sending back data
|
||||
const inner = async () => {
|
||||
await clientCreationPromise
|
||||
if (data.name === 'write') {
|
||||
skipWriteLog = true
|
||||
}
|
||||
client[data.name].bind(client)(...data.args)
|
||||
|
||||
if (data.name === 'registerChannel' && !registeredChannels.includes(data.args[0])) {
|
||||
client.on(data.args[0], (...args: any[]) => {
|
||||
emitEvent(data.args[0], ...args)
|
||||
})
|
||||
registeredChannels.push(data.args[0])
|
||||
}
|
||||
}
|
||||
void inner()
|
||||
},
|
||||
|
||||
async pingProxy (number: number) {
|
||||
return new Promise<number>((resolve) => {
|
||||
(client.socket as any)._ws.send(`ping:${number}`)
|
||||
const date = Date.now()
|
||||
const onPong = (received) => {
|
||||
if (received !== number.toString()) return
|
||||
client.socket.off('pong' as any, onPong)
|
||||
resolve(Date.now() - date)
|
||||
}
|
||||
client.socket.on('pong' as any, onPong)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const authFlowWorkerThread = async (client, options) => {
|
||||
self.postMessage({
|
||||
type: 'authFlow',
|
||||
version: client.version,
|
||||
username: client.username
|
||||
})
|
||||
options.onMsaCode = (data) => {
|
||||
self.postMessage({
|
||||
type: 'msaCode',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
client.authflow = {
|
||||
async getMinecraftJavaToken () {
|
||||
return new Promise(resolve => {
|
||||
self.addEventListener('message', async (e) => {
|
||||
if (e.data.type === 'authflowResult') {
|
||||
const restoredData = await restoreData(e.data.data)
|
||||
if (restoredData?.certificates?.profileKeys?.privatePEM) {
|
||||
restoredData.certificates.profileKeys.private = restoredData.certificates.profileKeys.privatePEM
|
||||
}
|
||||
resolve(restoredData)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
await Promise.race([
|
||||
protocolMicrosoftAuth.authenticate(client, options),
|
||||
// new Promise((_r, reject) => {
|
||||
// signInMessageState.abortController.signal.addEventListener('abort', () => {
|
||||
// reject(new UserError('Aborted by user'))
|
||||
// })
|
||||
// })
|
||||
])
|
||||
}
|
||||
|
||||
// restore dates from strings
|
||||
const restoreData = async (json) => {
|
||||
const promises = [] as Array<Promise<void>>
|
||||
if (typeof json === 'object' && json) {
|
||||
for (const [key, value] of Object.entries(json)) {
|
||||
if (typeof value === 'string') {
|
||||
promises.push(tryRestorePublicKey(value, key, json))
|
||||
if (value.endsWith('Z')) {
|
||||
const date = new Date(value)
|
||||
if (!isNaN(date.getTime())) {
|
||||
json[key] = date
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await restoreData(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
return json
|
||||
}
|
||||
|
||||
const tryRestorePublicKey = async (value: string, name: string, parent: { [x: string]: any }) => {
|
||||
value = value.trim()
|
||||
if (!name.endsWith('PEM') || !value.startsWith('-----BEGIN RSA PUBLIC KEY-----') || !value.endsWith('-----END RSA PUBLIC KEY-----')) return
|
||||
const der = pemToArrayBuffer(value)
|
||||
const key = await window.crypto.subtle.importKey(
|
||||
'spki', // Specify that the data is in SPKI format
|
||||
der,
|
||||
{
|
||||
name: 'RSA-OAEP',
|
||||
hash: { name: 'SHA-256' }
|
||||
},
|
||||
true,
|
||||
['encrypt'] // Specify key usages
|
||||
)
|
||||
const originalName = name.replace('PEM', '')
|
||||
const exported = await window.crypto.subtle.exportKey('spki', key)
|
||||
const exportedBuffer = new Uint8Array(exported)
|
||||
parent[originalName] = {
|
||||
export () {
|
||||
return exportedBuffer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pemToArrayBuffer (pem) {
|
||||
// Fetch the part of the PEM string between header and footer
|
||||
const pemHeader = '-----BEGIN RSA PUBLIC KEY-----'
|
||||
const pemFooter = '-----END RSA PUBLIC KEY-----'
|
||||
const pemContents = pem.slice(pemHeader.length, pem.length - pemFooter.length).trim()
|
||||
const binaryDerString = atob(pemContents.replaceAll(/\s/g, ''))
|
||||
const binaryDer = new Uint8Array(binaryDerString.length)
|
||||
for (let i = 0; i < binaryDerString.length; i++) {
|
||||
binaryDer[i] = binaryDerString.codePointAt(i)!
|
||||
}
|
||||
return binaryDer.buffer
|
||||
}
|
||||
|
||||
const syncProperties = [
|
||||
'version',
|
||||
'username',
|
||||
'uuid',
|
||||
'ended',
|
||||
'latency',
|
||||
'isServer'
|
||||
]
|
||||
|
||||
const setupPropertiesSync = (obj) => {
|
||||
sendProperties(obj, syncProperties)
|
||||
}
|
||||
|
||||
const sendProperties = (obj: any, properties: string[]) => {
|
||||
try {
|
||||
const sendObj = {}
|
||||
for (const property of properties) {
|
||||
sendObj[property] = obj[property]
|
||||
}
|
||||
self.postMessage({ type: 'properties', properties: sendObj })
|
||||
} catch (err) {
|
||||
// fallback to individual property send
|
||||
for (const property of properties) {
|
||||
try {
|
||||
self.postMessage({ type: 'properties', properties: { [property]: obj[property] } })
|
||||
} catch (err) {
|
||||
console.error('Failed to sync property (from worker)', property, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const expectedProperties = new Set([
|
||||
'version',
|
||||
])
|
||||
|
||||
const debugAnalyzeNeededProperties = (obj) => {
|
||||
const dummyEventEmitter = new EventEmitter()
|
||||
const dummyEventEmitterPrototype = Object.getPrototypeOf(dummyEventEmitter)
|
||||
const redundantProperties = Object.getOwnPropertyNames(obj).filter(property => !expectedProperties.has(property) && !(property in dummyEventEmitterPrototype))
|
||||
// console.log('redundantProperties', redundantProperties)
|
||||
}
|
||||
|
||||
const wrapClientSocket = (client: Client) => {
|
||||
const setupConnectHandlers = () => {
|
||||
net.Socket.prototype['handleStringMessage'] = function (message: string) {
|
||||
if (message.startsWith('proxy-message') || message.startsWith('proxy-command:')) { // for future
|
||||
return false
|
||||
}
|
||||
if (message.startsWith('proxy-shutdown:')) {
|
||||
lastKnownKickReason = message.slice('proxy-shutdown:'.length)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
client.socket.on('connect', () => {
|
||||
console.log('Proxy WebSocket connection established')
|
||||
//@ts-expect-error
|
||||
client.socket._ws.addEventListener('close', () => {
|
||||
console.log('WebSocket connection closed')
|
||||
// TODO important: for some reason close event of socket is never triggered now!
|
||||
setTimeout(() => {
|
||||
client.emit('end', lastKnownKickReason || 'WebSocket connection closed with unknown reason')
|
||||
}, 500)
|
||||
})
|
||||
client.socket.on('close', () => {
|
||||
setTimeout(() => {
|
||||
client.emit('end', lastKnownKickReason || 'WebSocket connection closed with unknown reason')
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
// socket setup actually can be delayed because of dns lookup
|
||||
if (client.socket) {
|
||||
setupConnectHandlers()
|
||||
} else {
|
||||
const originalSetSocket = client.setSocket.bind(client)
|
||||
client.setSocket = (socket) => {
|
||||
originalSetSocket(socket)
|
||||
setupConnectHandlers()
|
||||
}
|
||||
}
|
||||
}
|
||||
211
src/protocolWorker/protocolMain.ts
Normal file
211
src/protocolWorker/protocolMain.ts
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
import EventEmitter from 'events'
|
||||
import { Client, ClientOptions } from 'minecraft-protocol'
|
||||
import { useWorkerProxy } from 'renderer/viewer/lib/workerProxy'
|
||||
import { appQueryParams } from '../appParams'
|
||||
import { ConnectOptions } from '../connect'
|
||||
import { setLoadingScreenStatus } from '../appStatus'
|
||||
import { ParsedServerAddress } from '../parseServerAddress'
|
||||
import { authFlowMainThread, getAuthData } from './microsoftAuthflow'
|
||||
import type { PROXY_WORKER_TYPE } from './protocol.worker'
|
||||
|
||||
const debug = require('debug')('minecraft-protocol')
|
||||
|
||||
let protocolWorkerChannel: typeof PROXY_WORKER_TYPE['__workerProxy'] | undefined
|
||||
|
||||
export const getProtocolWorkerChannel = () => {
|
||||
return protocolWorkerChannel
|
||||
}
|
||||
|
||||
const copyPrimitiveValues = (obj: any, deep = false, ignoreKeys: string[] = []) => {
|
||||
const copy = {} as Record<string, any>
|
||||
for (const key in obj) {
|
||||
if (ignoreKeys.includes(key)) continue
|
||||
if (typeof obj[key] === 'object' && obj[key] !== null && deep) {
|
||||
copy[key] = copyPrimitiveValues(obj[key])
|
||||
} else if (typeof obj[key] === 'number' || typeof obj[key] === 'string' || typeof obj[key] === 'boolean') {
|
||||
copy[key] = obj[key]
|
||||
}
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
export const getProtocolClientGetter = async (proxy: { host: string, port?: string }, connectOptions: ConnectOptions, server: ParsedServerAddress) => {
|
||||
const cachedTokens = typeof connectOptions.authenticatedAccount === 'object' ? connectOptions.authenticatedAccount.cachedTokens : {}
|
||||
const authData = connectOptions.authenticatedAccount ?
|
||||
await getAuthData({
|
||||
tokenCaches: cachedTokens,
|
||||
proxyBaseUrl: connectOptions.proxy,
|
||||
setProgressText (text) {
|
||||
setLoadingScreenStatus(text)
|
||||
},
|
||||
connectingServer: server.serverIpFull.replace(/:25565$/, '')
|
||||
})
|
||||
: undefined
|
||||
|
||||
function createMinecraftProtocolClient (this: any) {
|
||||
if (!this.brand) return // brand is not resolved yet
|
||||
if (bot?._client) return bot._client
|
||||
const createClientOptions = copyPrimitiveValues(this, false, ['client']) as ClientOptions
|
||||
|
||||
createClientOptions.sessionServer = authData?.sessionEndpoint.toString()
|
||||
|
||||
const worker = new Worker(new URL('./protocol.worker.ts', import.meta.url))
|
||||
protocolWorkerChannel = useWorkerProxy<typeof PROXY_WORKER_TYPE>(worker)
|
||||
setTimeout(() => {
|
||||
if (bot) {
|
||||
bot.on('end', () => {
|
||||
worker.terminate()
|
||||
})
|
||||
} else {
|
||||
worker.terminate()
|
||||
}
|
||||
})
|
||||
|
||||
protocolWorkerChannel.setProxy({
|
||||
hostname: proxy.host,
|
||||
port: proxy.port ? +proxy.port : undefined
|
||||
})
|
||||
void protocolWorkerChannel.init({
|
||||
options: createClientOptions,
|
||||
noPacketsValidation: appQueryParams.noPacketsValidation === 'true',
|
||||
useAuthFlow: !!authData,
|
||||
isWebSocket: server.isWebSocket
|
||||
})
|
||||
|
||||
const eventEmitter = new EventEmitter() as any
|
||||
eventEmitter.version = this.version
|
||||
|
||||
worker.addEventListener('message', ({ data }) => {
|
||||
if (data.type === 'event') {
|
||||
eventEmitter.emit(data.event, ...data.args)
|
||||
if (data.event === 'packet') {
|
||||
let [packetData, packetMeta] = data.args
|
||||
if (window.stopPacketsProcessing === true || (Array.isArray(window.stopPacketsProcessing) && window.stopPacketsProcessing.includes(packetMeta.name))) {
|
||||
if (window.skipPackets && !window.skipPackets.includes(packetMeta.name)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Start timing the packet processing
|
||||
const startTime = performance.now()
|
||||
|
||||
// restore transferred data
|
||||
if (packetData instanceof Uint8Array) {
|
||||
packetData = Buffer.from(packetData)
|
||||
} else if (typeof packetData === 'object' && packetData !== null) {
|
||||
// Deep patch any Uint8Array values in the packet data object
|
||||
const patchUint8Arrays = (obj: any) => {
|
||||
for (const key in obj) {
|
||||
if (obj[key] instanceof Uint8Array) {
|
||||
obj[key] = Buffer.from(obj[key])
|
||||
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
|
||||
patchUint8Arrays(obj[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
patchUint8Arrays(packetData)
|
||||
}
|
||||
|
||||
eventEmitter.state = packetMeta.state
|
||||
debug(`RECV ${eventEmitter.state}:${packetMeta.name}`, packetData)
|
||||
|
||||
// Initialize packet timing tracking if not exists
|
||||
if (!window.packetTimings) {
|
||||
window.packetTimings = {}
|
||||
}
|
||||
|
||||
if (!window.packetTimings[packetMeta.name]) {
|
||||
window.packetTimings[packetMeta.name] = {
|
||||
total: 0,
|
||||
count: 0,
|
||||
avg: 0
|
||||
}
|
||||
}
|
||||
|
||||
eventEmitter.emit(packetMeta.name, packetData, packetMeta)
|
||||
|
||||
// Calculate processing time
|
||||
const processingTime = performance.now() - startTime
|
||||
window.packetTimings[packetMeta.name].total += processingTime
|
||||
window.packetTimings[packetMeta.name].count++
|
||||
window.packetTimings[packetMeta.name].avg =
|
||||
window.packetTimings[packetMeta.name].total / window.packetTimings[packetMeta.name].count
|
||||
|
||||
// Update packetsThreadBlocking every second
|
||||
if (!window.lastStatsUpdate) {
|
||||
window.lastStatsUpdate = Date.now()
|
||||
setInterval(() => {
|
||||
// Sort by total processing time
|
||||
window.packetsThreadBlocking = Object.entries(window.packetTimings)
|
||||
.sort(([, a], [, b]) => b.total - a.total)
|
||||
.reduce((acc, [key, value]) => {
|
||||
acc[key] = value
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
// Reset timings for next interval
|
||||
window.packetTimings = {}
|
||||
window.lastStatsUpdate = Date.now()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.type === 'properties') {
|
||||
// eslint-disable-next-line guard-for-in
|
||||
for (const property in data.properties) {
|
||||
eventEmitter[property] = data.properties[property]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
eventEmitter.on('writePacket', (...args: any[]) => {
|
||||
debug(`SEND ${eventEmitter.state}:${args[0]}`, ...args.slice(1))
|
||||
})
|
||||
|
||||
const redirectMethodsToWorker = (names: string[]) => {
|
||||
for (const name of names) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
eventEmitter[name] = async (...args: any[]) => {
|
||||
protocolWorkerChannel?.call({
|
||||
name,
|
||||
args: JSON.parse(JSON.stringify(args))
|
||||
})
|
||||
|
||||
if (name === 'write') {
|
||||
eventEmitter.emit('writePacket', ...args)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
redirectMethodsToWorker([
|
||||
'write',
|
||||
'writeRaw',
|
||||
'writeChannel',
|
||||
'registerChannel',
|
||||
'unregisterChannel',
|
||||
'chat',
|
||||
'reportPlayer',
|
||||
'end'
|
||||
])
|
||||
|
||||
if (authData) {
|
||||
void authFlowMainThread(worker, authData, connectOptions, (onJoin) => {
|
||||
eventEmitter.on('login', onJoin)
|
||||
})
|
||||
}
|
||||
|
||||
return eventEmitter
|
||||
// return new Proxy(eventEmitter, {
|
||||
// get (target, prop) {
|
||||
// if (!(prop in target)) {
|
||||
// // console.warn(`Accessing non-existent property "${String(prop)}" on event emitter`)
|
||||
// }
|
||||
// const value = target[prop]
|
||||
// return typeof value === 'function' ? value.bind(target) : value
|
||||
// }
|
||||
// })
|
||||
}
|
||||
return createMinecraftProtocolClient
|
||||
}
|
||||
6
src/protocolWorker/protocolWorkerGlobals.ts
Normal file
6
src/protocolWorker/protocolWorkerGlobals.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// '/'+location.pathname.split('/').slice(0, -4).join('/')
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
globalThis.window = self
|
||||
//@ts-expect-error
|
||||
process.versions = { node: '' }
|
||||
|
|
@ -4,7 +4,7 @@ import { activeModalStack, activeModalStacks, hideModal, insertActiveModalStack,
|
|||
import { guessProblem } from '../errorLoadingScreenHelpers'
|
||||
import type { ConnectOptions } from '../connect'
|
||||
import { downloadPacketsReplay, packetsRecordingState, replayLogger } from '../packetsReplay/packetsReplayLegacy'
|
||||
import { getProxyDetails } from '../microsoftAuthflow'
|
||||
import { getProxyDetails } from '../protocolWorker/microsoftAuthflow'
|
||||
import { downloadAutoCapturedPackets, getLastAutoCapturedPackets } from '../mineflayer/plugins/packetsRecording'
|
||||
import { appQueryParams } from '../appParams'
|
||||
import AppStatus from './AppStatus'
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { versionToNumber } from 'renderer/viewer/common/utils'
|
||||
import { toMajorVersion } from 'renderer/viewer/lib/simpleUtils'
|
||||
import { restoreMinecraftData } from '../optimizeJson'
|
||||
// import minecraftInitialDataJson from '../../generated/minecraft-initial-data.json'
|
||||
import { toMajorVersion } from '../utils'
|
||||
import { importLargeData } from '../../generated/large-data-aliases'
|
||||
|
||||
const customResolver = () => {
|
||||
|
|
|
|||
|
|
@ -128,12 +128,6 @@ export const isMajorVersionGreater = (ver1: string, ver2: string) => {
|
|||
return +a1 > +a2 || (+a1 === +a2 && +b1 > +b2)
|
||||
}
|
||||
|
||||
// doesn't support snapshots
|
||||
export const toMajorVersion = version => {
|
||||
const [a, b] = (String(version)).split('.')
|
||||
return `${a}.${b}`
|
||||
}
|
||||
|
||||
let prevRenderDistance = options.renderDistance
|
||||
export const setRenderDistance = () => {
|
||||
assertDefined(worldView)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue