Compare commits

...
Sign in to create a new pull request.

91 commits

Author SHA1 Message Date
Vitaly Turovsky
139ee9ad1f Revert "fix single file build"
This reverts commit cd84acbe7a.
2025-05-03 22:41:13 +03:00
Vitaly Turovsky
cd84acbe7a fix single file build 2025-05-01 13:31:17 +03:00
Vitaly Turovsky
1fe6239b44 Merge branch 'next' into protocol-worker 2025-04-18 23:53:11 +03:00
Vitaly Turovsky
d9f1efda10 fix 2025-04-12 04:42:30 +03:00
Vitaly Turovsky
475d990217 patch 2025-04-12 04:28:32 +03:00
Vitaly
4a949d3c14
Update rsbuild.config.ts
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-04-12 04:17:37 +03:00
Vitaly
ccd1130282
Update rsbuild.config.ts
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-04-12 04:16:34 +03:00
Vitaly Turovsky
df2a337af5 try fixing single file, no more tries for now 2025-04-12 03:42:42 +03:00
Vitaly Turovsky
92d444af62 fix one build, break another 2025-04-12 03:36:56 +03:00
Vitaly Turovsky
b50215b1fa fix build 2025-04-12 03:30:07 +03:00
Vitaly Turovsky
980779ce68 revertconfig 2025-04-12 02:59:01 +03:00
Vitaly Turovsky
4483b73adf revert 2025-04-12 02:58:30 +03:00
Vitaly Turovsky
316fdd87af Merge remote-tracking branch 'origin/next' into protocol-worker 2025-04-12 02:57:38 +03:00
Vitaly Turovsky
bdcf3d62fe finishing! fix all known bugs! chat! websockets! fix ping & username crash! 2025-04-12 02:56:25 +03:00
Vitaly Turovsky
078744000e . 2025-04-10 21:42:22 +03:00
Vitaly Turovsky
290e64150c Merge remote-tracking branch 'origin/next' into protocol-worker 2025-04-10 21:42:15 +03:00
Vitaly Turovsky
07e9725b2d . 2025-03-31 08:21:39 +03:00
Vitaly Turovsky
bb6faf9b24 Merge remote-tracking branch 'origin/nextgen-physics' into protocol-worker 2025-03-30 18:02:55 +03:00
Vitaly Turovsky
0839889d21 Merge remote-tracking branch 'origin/next' into nextgen-physics 2025-03-30 17:29:31 +03:00
Vitaly Turovsky
07e7c6f227 revert physics for test 2025-03-26 06:47:04 +03:00
Vitaly Turovsky
bc9ca189cd restore2 2025-03-26 05:14:30 +03:00
Vitaly Turovsky
b9ca0573ac restore 2025-03-26 05:14:09 +03:00
Vitaly Turovsky
e743a039a3 Merge branch 'nextgen-physics' into protocol-worker 2025-03-26 05:13:32 +03:00
Vitaly Turovsky
b47258301c revert unknown chaange 2025-03-26 05:13:15 +03:00
Vitaly Turovsky
0648d55edd pick only necessary changes 2025-03-26 05:11:37 +03:00
Vitaly Turovsky
e7c240694f up 2025-03-26 05:03:37 +03:00
Vitaly Turovsky
14e20a2cf6 Merge remote-tracking branch 'origin/next' into nextgen-physics 2025-03-26 05:02:32 +03:00
Vitaly Turovsky
9057d3acb5 revert stuff 2025-03-26 00:05:55 +03:00
Vitaly Turovsky
547658f489 Merge remote-tracking branch 'origin/next' into nextgen-physics 2025-03-26 00:03:28 +03:00
Vitaly Turovsky
bf9c47dd26 fix test 2025-03-26 00:01:28 +03:00
Vitaly Turovsky
9cede6dbbc final fixes 2025-03-26 00:01:28 +03:00
Vitaly Turovsky
6a5ac4f8d2 make initial resource pack load optimised, rework it 2025-03-26 00:01:28 +03:00
Vitaly Turovsky
2630a57d35 renderers cleanup 2025-03-26 00:01:28 +03:00
Vitaly Turovsky
20569747ca fix mesher config pass 2025-03-26 00:01:27 +03:00
Vitaly Turovsky
9888bd55c1 fix: allow to go back when not crashed game 2025-03-26 00:01:27 +03:00
Vitaly
b501893ab2 ip pkgs 2025-03-23 03:13:52 +00:00
Vitaly Turovsky
e2b78333a1 Merge remote-tracking branch 'origin/renderer-rewrite' into nextgen-physics 2025-03-21 16:33:42 +03:00
Vitaly Turovsky
e917764b76 fix lint 2025-03-21 13:58:53 +03:00
Vitaly Turovsky
ed041972c4 disable in other way 2025-03-21 13:50:11 +03:00
Vitaly Turovsky
b579ee1767 Revert "disable playground"
This reverts commit 5eedb3c456.
2025-03-21 13:48:39 +03:00
Vitaly Turovsky
d450a31547 fix typo 2025-03-21 11:27:12 +03:00
Vitaly Turovsky
853e0e1d84 up readme 2025-03-21 11:05:19 +03:00
Vitaly Turovsky
8ddac97414 dont use bot 2025-03-21 11:02:30 +03:00
Vitaly Turovsky
5eedb3c456 disable playground 2025-03-21 10:59:46 +03:00
Vitaly Turovsky
11abbfcbb1 disable check 2025-03-21 07:07:56 +03:00
Vitaly Turovsky
8ee4dc37e7 disable displaying unknown non interactible entitites 2025-03-21 07:07:26 +03:00
Vitaly Turovsky
dc2ad7ccce disable signs, disable playground since its not used by anyone 2025-03-21 06:36:15 +03:00
Vitaly Turovsky
b483923009 rm unused three imports 2025-03-21 06:22:30 +03:00
Vitaly Turovsky
82d0638eb8 Merge remote-tracking branch 'origin/next' into renderer-rewrite 2025-03-21 06:20:58 +03:00
Vitaly Turovsky
cae2b612ba fix all remaining reactive state linking 2025-03-21 06:19:36 +03:00
Vitaly Turovsky
f88e9c8b61 Refactor renderer state management and move vr 2025-03-21 06:11:31 +03:00
Vitaly Turovsky
136b051695 smooth camera movement! 2025-03-21 05:16:30 +03:00
Vitaly Turovsky
9fedafe776 move threejs entities & cursor block to renderer 2025-03-21 05:07:27 +03:00
Vitaly Turovsky
ccb00043cf fix reload hand 2025-03-20 22:38:23 +03:00
Vitaly Turovsky
c1a7765fcb final code cleanup i think 2025-03-20 22:10:14 +03:00
Vitaly Turovsky
de3eddad89 a lot of imports update, data cleanup, add thousands errors 2025-03-20 05:36:22 +03:00
Vitaly Turovsky
e851f4fac2 Merge remote-tracking branch 'origin/next' into nextgen-physics 2025-03-20 05:15:31 +03:00
Vitaly Turovsky
f2f1c2538e 10x inventory performance 2025-03-20 04:53:52 +03:00
Vitaly Turovsky
5364085030 fix possible crash on non existing server data update 2025-03-20 04:37:02 +03:00
Vitaly Turovsky
400f5982be update server data for index 0 2025-03-20 04:34:55 +03:00
Vitaly Turovsky
dc073cd559 Merge remote-tracking branch 'origin/next' into renderer-rewrite 2025-03-19 01:26:02 +03:00
Vitaly Turovsky
6eb50cde24 last dont crash 2025-03-19 01:25:03 +03:00
Vitaly Turovsky
67d90a56fb Merge remote-tracking branch 'origin/next' into renderer-rewrite 2025-03-19 01:24:34 +03:00
Vitaly Turovsky
f2307632a2 Merge remote-tracking branch 'origin/next' into nextgen-physics 2025-03-18 20:51:34 +03:00
Vitaly Turovsky
d74d860726 Merge remote-tracking branch 'origin/next' into renderer-rewrite 2025-03-15 05:36:54 +03:00
Vitaly Turovsky
4d4637f710 Merge remote-tracking branch 'origin/next' into nextgen-physics 2025-03-15 04:33:12 +03:00
Vitaly Turovsky
847314d50f hide hand in spectator 2025-03-15 02:18:51 +03:00
Vitaly Turovsky
8a3c84745d test flying! 2025-03-15 02:18:44 +03:00
Vitaly Turovsky
3a9e2aa384 fix 2025-03-15 00:46:21 +03:00
Vitaly Turovsky
7cc562bd02 Merge remote-tracking branch 'origin/next' into nextgen-physics 2025-03-15 00:45:55 +03:00
Vitaly Turovsky
0597a3dad2 make library versions less annoying 2025-03-09 04:57:48 +03:00
Vitaly
cefdf5362f Merge branch 'next' into nextgen-physics 2025-03-07 15:59:22 +00:00
Vitaly Turovsky
d197859d47 big resourcemanager refactor 2025-03-05 15:30:04 +03:00
Vitaly Turovsky
1861edf567 delayLoadUntilFocus 2025-03-04 14:17:44 +03:00
Vitaly Turovsky
d8294d565b even more appViewer usage 2025-03-04 13:53:28 +03:00
Vitaly Turovsky
4381ef4f75 removing bobbing & panorama 2025-03-04 12:57:04 +03:00
Vitaly Turovsky
c65db9a8cb a working refactor 2025-03-04 12:12:42 +03:00
Vitaly Turovsky
34972e4e71 Merge branch 'next' into nextgen-physics 2025-02-27 05:16:21 +03:00
Vitaly
638dd6711e
Merge branch 'next' into nextgen-physics 2025-02-15 05:31:40 +03:00
Vitaly
9356daaefc
Merge branch 'next' into nextgen-physics 2025-02-13 23:27:44 +03:00
Vitaly Turovsky
fb10179691 set 1.19.4 for now! 2025-02-03 10:26:24 +03:00
Vitaly Turovsky
7e74633c14 Merge branch 'next' into nextgen-physics 2025-02-03 10:25:47 +03:00
Vitaly Turovsky
2d7ec12a75 Merge branch 'next' into nextgen-physics 2025-02-03 10:25:08 +03:00
Vitaly Turovsky
c626d105ff use actually useful server 2025-01-31 18:44:26 +03:00
Vitaly Turovsky
8db6b5bb51 fix library versions 2025-01-31 06:00:50 +03:00
Vitaly Turovsky
2848ab63d3 disable silly creative fly patching 2025-01-31 05:30:59 +03:00
Vitaly Turovsky
537658476d use gen mineflayer 2025-01-30 21:38:35 +03:00
Vitaly Turovsky
044153c2dc up ver 2025-01-30 21:36:23 +03:00
Vitaly Turovsky
380c21486b make it finally pass grim checks! 2025-01-30 21:33:30 +03:00
Vitaly Turovsky
aed5b40516 use local 2025-01-29 03:31:16 +03:00
Vitaly Turovsky
3051cc35f5 up pkg 2025-01-29 03:30:25 +03:00
17 changed files with 704 additions and 224 deletions

View file

@ -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": [
{

View file

@ -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) {

View file

@ -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}`
}

View file

@ -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)
}
}
}
}
})

View file

@ -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'

View file

@ -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')
}

View file

@ -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}`)

View file

@ -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(() => {

View file

@ -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}`)

View file

@ -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,

View file

@ -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}`

View 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()
}
}
}

View 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
}

View 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: '' }

View file

@ -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'

View file

@ -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 = () => {

View file

@ -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)