From 0dcfbe481a0b37dd29e040a4befe51a47ed172ad Mon Sep 17 00:00:00 2001 From: Vitaly Date: Thu, 9 May 2024 03:46:21 +0300 Subject: [PATCH 1/8] docs: Add query string options for loading map and setting values --- README.MD | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.MD b/README.MD index bc39fb98f..ce3685400 100644 --- a/README.MD +++ b/README.MD @@ -114,6 +114,9 @@ Press `Y` to set query parameters to url of your current game state. - `?singleplayer=1` - Create empty world on load. Nothing will be saved - `?noSave=true` - Disable auto save on unload / disconnect / export. Only manual save with `/save` command will work +- `?map=` - Load the map from ZIP. You can use any url, but it must be CORS enabled. +- `?setting=:` - Set the and lock the setting on load. You can set multiple settings by separating them with `&` e.g. `?setting=autoParkour:true&setting=renderDistance:4` + ### Notable Things that Power this Project - [Mineflayer](https://github.com/PrismarineJS/mineflayer) - Handles all client-side communications with the server (including the builtin one) - forked From e56cac8b43284a8c9f4ca750ef44d2c0a82a37ac Mon Sep 17 00:00:00 2001 From: Vitaly Date: Fri, 10 May 2024 02:26:10 +0300 Subject: [PATCH 2/8] refactor: use source mc protocol and patch it instead fix: fix issues with 1.20.4 fixes #80 --- package.json | 14 ++- patches/minecraft-protocol@1.47.0.patch | 130 ++++++++++++++++++++++++ pnpm-lock.yaml | 63 ++++++++++-- 3 files changed, 194 insertions(+), 13 deletions(-) create mode 100644 patches/minecraft-protocol@1.47.0.patch diff --git a/package.json b/package.json index acee3c832..2e3b9dbaf 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "esbuild": "^0.19.3", "esbuild-plugin-polyfill-node": "^0.3.0", "express": "^4.18.2", + "filesize": "^10.0.12", "flying-squid": "npm:@zardoy/flying-squid@^0.0.20", "fs-extra": "^11.1.1", "google-drive-browserfs": "github:zardoy/browserfs#google-drive", @@ -70,6 +71,8 @@ "lodash-es": "^4.17.21", "minecraft-assets": "^1.12.2", "minecraft-data": "3.65.0", + "minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol", + "mojangson": "^2.0.4", "net-browserify": "github:zardoy/prismarinejs-net-browserify", "node-gzip": "^1.1.2", "peerjs": "^1.5.0", @@ -77,6 +80,7 @@ "prismarine-provider-anvil": "github:zardoy/prismarine-provider-anvil#everything", "prosemirror-example-setup": "^1.2.2", "prosemirror-markdown": "^1.12.0", + "prosemirror-menu": "^1.2.4", "prosemirror-state": "^1.4.3", "prosemirror-view": "^1.33.1", "qrcode.react": "^3.1.0", @@ -84,18 +88,15 @@ "react-dom": "^18.2.0", "react-transition-group": "^4.4.5", "remark": "^15.0.1", - "filesize": "^10.0.12", "sanitize-filename": "^1.6.3", "skinview3d": "^3.0.1", "source-map-js": "^1.0.2", "stats-gl": "^1.0.5", "stats.js": "^0.17.0", - "use-typed-event-listener": "^4.0.2", - "mojangson": "^2.0.4", - "prosemirror-menu": "^1.2.4", "tabbable": "^6.2.0", "title-case": "3.x", "ua-parser-js": "^1.0.37", + "use-typed-event-listener": "^4.0.2", "valtio": "^1.11.1", "vec3": "^0.1.7", "workbox-build": "^7.0.0" @@ -156,12 +157,15 @@ "prismarine-world": "github:zardoy/prismarine-world#next-era", "minecraft-data": "3.65.0", "prismarine-provider-anvil": "github:zardoy/prismarine-provider-anvil#everything", - "minecraft-protocol": "github:zardoy/minecraft-protocol#everything", + "minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol", "react": "^18.2.0", "prismarine-chunk": "github:zardoy/prismarine-chunk" }, "updateConfig": { "ignoreDependencies": [] + }, + "patchedDependencies": { + "minecraft-protocol@1.47.0": "patches/minecraft-protocol@1.47.0.patch" } }, "packageManager": "pnpm@9.0.4" diff --git a/patches/minecraft-protocol@1.47.0.patch b/patches/minecraft-protocol@1.47.0.patch new file mode 100644 index 000000000..02bbdd5dd --- /dev/null +++ b/patches/minecraft-protocol@1.47.0.patch @@ -0,0 +1,130 @@ +diff --git a/src/client/autoVersion.js b/src/client/autoVersion.js +index c437ecf3a0e4ab5758a48538c714b7e9651bb5da..d9c9895ae8614550aa09ad60a396ac32ffdf1287 100644 +--- a/src/client/autoVersion.js ++++ b/src/client/autoVersion.js +@@ -9,7 +9,7 @@ module.exports = function (client, options) { + client.wait_connect = true // don't let src/client/setProtocol proceed on socket 'connect' until 'connect_allowed' + debug('pinging', options.host) + // TODO: use 0xfe ping instead for better compatibility/performance? https://github.com/deathcap/node-minecraft-ping +- ping(options, function (err, response) { ++ ping(options, async function (err, response) { + if (err) { return client.emit('error', err) } + debug('ping response', response) + // TODO: could also use ping pre-connect to save description, type, max players, etc. +@@ -40,6 +40,7 @@ module.exports = function (client, options) { + + // Reinitialize client object with new version TODO: move out of its constructor? + client.version = minecraftVersion ++ await options.versionSelectedHook?.(client) + client.state = states.HANDSHAKING + + // Let other plugins such as Forge/FML (modinfo) respond to the ping response +diff --git a/src/client/encrypt.js b/src/client/encrypt.js +index b9d21bab9faccd5dbf1975fc423fc55c73e906c5..99ffd76527b410e3a393181beb260108f4c63536 100644 +--- a/src/client/encrypt.js ++++ b/src/client/encrypt.js +@@ -25,7 +25,11 @@ module.exports = function (client, options) { + if (packet.serverId !== '-') { + debug('This server appears to be an online server and you are providing no password, the authentication will probably fail') + } +- sendEncryptionKeyResponse() ++ client.end('This server appears to be an online server and you are providing no authentication. Try authenticating first.') ++ // sendEncryptionKeyResponse() ++ // client.once('set_compression', () => { ++ // clearTimeout(loginTimeout) ++ // }) + } + + function onJoinServerResponse (err) { +diff --git a/src/client.js b/src/client.js +index c89375e32babbf3559655b1e95f6441b9a30796f..f24cd5dc8fa9a0a4000b184fb3c79590a3ad8b8a 100644 +--- a/src/client.js ++++ b/src/client.js +@@ -88,10 +88,12 @@ class Client extends EventEmitter { + parsed.metadata.name = parsed.data.name + parsed.data = parsed.data.params + parsed.metadata.state = state +- debug('read packet ' + state + '.' + parsed.metadata.name) +- if (debug.enabled) { +- const s = JSON.stringify(parsed.data, null, 2) +- debug(s && s.length > 10000 ? parsed.data : s) ++ if (!globalThis.excludeCommunicationDebugEvents?.includes(parsed.metadata.name)) { ++ debug('read packet ' + state + '.' + parsed.metadata.name) ++ if (debug.enabled) { ++ const s = JSON.stringify(parsed.data, null, 2) ++ debug(s && s.length > 10000 ? parsed.data : s) ++ } + } + if (this._hasBundlePacket && parsed.metadata.name === 'bundle_delimiter') { + if (this._mcBundle.length) { // End bundle +@@ -109,7 +111,13 @@ class Client extends EventEmitter { + this._hasBundlePacket = false + } + } else { +- emitPacket(parsed) ++ try { ++ emitPacket(parsed) ++ } catch (err) { ++ console.log('Client incorrectly handled packet ' + parsed.metadata.name) ++ console.error(err) ++ // todo investigate why it doesn't close the stream even if unhandled there ++ } + } + }) + } +@@ -166,7 +174,10 @@ class Client extends EventEmitter { + } + + const onFatalError = (err) => { +- this.emit('error', err) ++ // todo find out what is trying to write after client disconnect ++ if(err.code !== 'ECONNABORTED') { ++ this.emit('error', err) ++ } + endSocket() + } + +@@ -195,6 +206,8 @@ class Client extends EventEmitter { + serializer -> framer -> socket -> splitter -> deserializer */ + if (this.serializer) { + this.serializer.end() ++ this.socket?.end() ++ this.socket?.emit('end') + } else { + if (this.socket) this.socket.end() + } +@@ -236,8 +249,11 @@ class Client extends EventEmitter { + + write (name, params) { + if (!this.serializer.writable) { return } +- debug('writing packet ' + this.state + '.' + name) +- debug(params) ++ if (!globalThis.excludeCommunicationDebugEvents?.includes(name)) { ++ debug(`[${this.state}] from ${this.isServer ? 'server' : 'client'}: ` + name) ++ debug(params) ++ } ++ this.emit('writePacket', name, params) + this.serializer.write({ name, params }) + } + +diff --git a/src/index.d.ts b/src/index.d.ts +index 0a5821c32d735e11205a280aa5a503c13533dc14..94a49f661d922478b940d853169b6087e6ec3df5 100644 +--- a/src/index.d.ts ++++ b/src/index.d.ts +@@ -121,6 +121,7 @@ declare module 'minecraft-protocol' { + sessionServer?: string + keepAlive?: boolean + closeTimeout?: number ++ closeTimeout?: number + noPongTimeout?: number + checkTimeoutInterval?: number + version?: string +@@ -141,6 +142,8 @@ declare module 'minecraft-protocol' { + disableChatSigning?: boolean + /** Pass custom client implementation if needed. */ + Client?: Client ++ /** Can be used to prepare mc data on autoVersion (client.version has selected version) */ ++ versionSelectedHook?: (client: Client) => Promise | void + } + + export class Server extends EventEmitter { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 36cc8e307..e0a424e96 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,10 +12,15 @@ overrides: prismarine-world: github:zardoy/prismarine-world#next-era minecraft-data: 3.65.0 prismarine-provider-anvil: github:zardoy/prismarine-provider-anvil#everything - minecraft-protocol: github:zardoy/minecraft-protocol#everything + minecraft-protocol: github:PrismarineJS/node-minecraft-protocol react: ^18.2.0 prismarine-chunk: github:zardoy/prismarine-chunk +patchedDependencies: + minecraft-protocol@1.47.0: + hash: 2uxevyasyasdavsxuehfavgkjq + path: patches/minecraft-protocol@1.47.0.patch + importers: .: @@ -122,6 +127,9 @@ importers: minecraft-data: specifier: 3.65.0 version: 3.65.0 + minecraft-protocol: + specifier: github:PrismarineJS/node-minecraft-protocol + version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/ccab9fb39681f3ebe0d264e2a3f833aa3c5a1ac7(patch_hash=2uxevyasyasdavsxuehfavgkjq)(encoding@0.1.13) mojangson: specifier: ^2.0.4 version: 2.0.4 @@ -6038,9 +6046,13 @@ packages: resolution: {tarball: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/5554c7ab0a74bce52aa5f5f04a48eb8d3b9ac65c} version: 1.0.1 - minecraft-protocol@https://codeload.github.com/zardoy/minecraft-protocol/tar.gz/2c14a686bfe7cbd9a5c87b629b402295ee86219f: - resolution: {tarball: https://codeload.github.com/zardoy/minecraft-protocol/tar.gz/2c14a686bfe7cbd9a5c87b629b402295ee86219f} - version: 1.45.0 + minecraft-protocol@1.47.0: + resolution: {integrity: sha512-IHL8faXLLIWv1O+2v2NgyKlooilu/OiSL9orI8Kqed/rZvVOrFPzs2PwMAYjpQX9gxLPhiSU19KqZ8CjfNuqhg==} + engines: {node: '>=14'} + + minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/ccab9fb39681f3ebe0d264e2a3f833aa3c5a1ac7: + resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/ccab9fb39681f3ebe0d264e2a3f833aa3c5a1ac7} + version: 1.47.0 engines: {node: '>=14'} minecraft-wrap@1.5.1: @@ -6698,6 +6710,9 @@ packages: resolution: {tarball: https://codeload.github.com/zardoy/prismarine-block/tar.gz/ada4ec3fdfbbc1cc20ab01d0e23f0718a77cc1a0} version: 1.17.1 + prismarine-chat@1.10.1: + resolution: {integrity: sha512-XukYcuueuhDxzEXG7r8BZyt6jOObrPPB4JESCgb+/XenB9nExoSHF8eTQWWj8faKPLqm1dRQaYwFJlNBlJZJUw==} + prismarine-chat@1.9.1: resolution: {integrity: sha512-x7WWa5MNhiLZSO6tw+YyKpzquFZ+DNISVgiV6K3SU0GsishMXe+nto02WhF/4AuFerKdugm9u1d/r4C4zSkJOg==} @@ -11924,7 +11939,7 @@ snapshots: flatmap: 0.0.3 long: 5.2.3 minecraft-data: 3.65.0 - minecraft-protocol: https://codeload.github.com/zardoy/minecraft-protocol/tar.gz/2c14a686bfe7cbd9a5c87b629b402295ee86219f(encoding@0.1.13) + minecraft-protocol: 1.47.0(patch_hash=2uxevyasyasdavsxuehfavgkjq)(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 @@ -15670,7 +15685,7 @@ snapshots: - '@types/react' - react - minecraft-protocol@https://codeload.github.com/zardoy/minecraft-protocol/tar.gz/2c14a686bfe7cbd9a5c87b629b402295ee86219f(encoding@0.1.13): + minecraft-protocol@1.47.0(patch_hash=2uxevyasyasdavsxuehfavgkjq)(encoding@0.1.13): dependencies: '@types/readable-stream': 4.0.12 aes-js: 3.1.2 @@ -15684,6 +15699,32 @@ snapshots: node-fetch: 2.7.0(encoding@0.1.13) node-rsa: 0.4.2 prismarine-auth: 2.4.2(encoding@0.1.13) + prismarine-chat: 1.10.1 + prismarine-nbt: 2.5.0 + prismarine-realms: 1.3.2(encoding@0.1.13) + protodef: 1.15.0 + readable-stream: 4.5.2 + uuid-1345: 1.0.2 + yggdrasil: 1.7.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + - supports-color + + minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/ccab9fb39681f3ebe0d264e2a3f833aa3c5a1ac7(patch_hash=2uxevyasyasdavsxuehfavgkjq)(encoding@0.1.13): + dependencies: + '@types/readable-stream': 4.0.12 + aes-js: 3.1.2 + buffer-equal: 1.0.1 + debug: 4.3.4(supports-color@8.1.1) + endian-toggle: 0.0.0 + lodash.get: 4.4.2 + lodash.merge: 4.6.2 + minecraft-data: 3.65.0 + minecraft-folder-path: 1.2.0 + node-fetch: 2.7.0(encoding@0.1.13) + node-rsa: 0.4.2 + prismarine-auth: 2.4.2(encoding@0.1.13) + prismarine-chat: 1.10.1 prismarine-nbt: 2.5.0 prismarine-realms: 1.3.2(encoding@0.1.13) protodef: 1.15.0 @@ -15730,7 +15771,7 @@ snapshots: mineflayer@https://codeload.github.com/PrismarineJS/mineflayer/tar.gz/5a544cf2547a6e0f1f17786962d77a33c661c02f(encoding@0.1.13): dependencies: minecraft-data: 3.65.0 - minecraft-protocol: https://codeload.github.com/zardoy/minecraft-protocol/tar.gz/2c14a686bfe7cbd9a5c87b629b402295ee86219f(encoding@0.1.13) + minecraft-protocol: 1.47.0(patch_hash=2uxevyasyasdavsxuehfavgkjq)(encoding@0.1.13) prismarine-biome: 1.3.0(minecraft-data@3.65.0)(prismarine-registry@1.7.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/ada4ec3fdfbbc1cc20ab01d0e23f0718a77cc1a0 prismarine-chat: 1.9.1 @@ -16445,6 +16486,12 @@ snapshots: prismarine-nbt: 2.5.0 prismarine-registry: 1.7.0 + prismarine-chat@1.10.1: + dependencies: + mojangson: 2.0.4 + prismarine-nbt: 2.5.0 + prismarine-registry: 1.7.0 + prismarine-chat@1.9.1: dependencies: mojangson: 2.0.4 @@ -16467,7 +16514,7 @@ snapshots: prismarine-entity@2.3.1: dependencies: - prismarine-chat: 1.9.1 + prismarine-chat: 1.10.1 prismarine-item: 1.14.0 prismarine-registry: 1.7.0 vec3: 0.1.8 From 08782b2695e505cb9790db4d4efcdb35597aae93 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Fri, 10 May 2024 02:51:59 +0300 Subject: [PATCH 3/8] fix: version override was always ignored when connecting from servers list UI fix: default proxy was not selected on setting a server ip via QS params --- config.json | 2 ++ src/index.ts | 28 +++++++++++++++++++++++++--- src/react/AddServerOrConnect.tsx | 12 ++++++------ src/react/ServersList.tsx | 2 +- src/react/ServersListProvider.tsx | 23 +++++++++-------------- 5 files changed, 43 insertions(+), 24 deletions(-) diff --git a/config.json b/config.json index c1db50c3a..1bbbfd472 100644 --- a/config.json +++ b/config.json @@ -6,6 +6,7 @@ "promoteServers": [ { "ip": "kaboom.pw", + "version": "1.18.2", "description": "Chaos and destruction server. Free for everyone." }, { @@ -15,6 +16,7 @@ }, { "ip": "play.minemalia.com", + "version": "1.18.2", "description": "Only login with existing accounts." } ] diff --git a/src/index.ts b/src/index.ts index 0cd178d95..f63e45d78 100644 --- a/src/index.ts +++ b/src/index.ts @@ -42,8 +42,10 @@ import { defaultsDeep } from 'lodash-es' import { initVR } from './vr' import { + AppConfig, activeModalStack, activeModalStacks, + hideModal, insertActiveModalStack, isGameActive, miscUiState, @@ -88,6 +90,7 @@ import { ViewerWrapper } from 'prismarine-viewer/viewer/lib/viewerWrapper' import './devReload' import './water' import { ConnectOptions } from './connect' +import { subscribe } from 'valtio' window.debug = debug window.THREE = THREE @@ -832,7 +835,7 @@ document.body.addEventListener('touchstart', (e) => { void window.fetch('config.json').then(async res => res.json()).then(c => c, (error) => { console.warn('Failed to load optional app config.json', error) return {} -}).then((config) => { +}).then((config: AppConfig | {}) => { miscUiState.appConfig = config }) @@ -850,8 +853,27 @@ downloadAndOpenFile().then((downloadAction) => { return } if (qs.get('ip') || qs.get('proxy')) { - // show server editor for connect or save - showModal({ reactType: 'editServer' }) + const waitAppConfigLoad = !qs.get('proxy') + const openServerEditor = () => { + hideModal() + // show server editor for connect or save + showModal({ reactType: 'editServer' }) + } + showModal({ reactType: 'empty' }) + if (waitAppConfigLoad) { + const unsubscribe = subscribe(miscUiState, checkCanDisplay) + checkCanDisplay() + // eslint-disable-next-line no-inner-declarations + function checkCanDisplay () { + if (miscUiState.appConfig) { + unsubscribe() + openServerEditor() + return true + } + } + } else { + openServerEditor() + } } void Promise.resolve().then(() => { diff --git a/src/react/AddServerOrConnect.tsx b/src/react/AddServerOrConnect.tsx index 54f04af18..500c1e610 100644 --- a/src/react/AddServerOrConnect.tsx +++ b/src/react/AddServerOrConnect.tsx @@ -4,7 +4,7 @@ import Input from './Input' import Button from './Button' import { useIsSmallWidth } from './simpleHooks' -export interface NewServerInfo { +export interface BaseServerInfo { ip: string name?: string versionOverride?: string @@ -15,12 +15,12 @@ export interface NewServerInfo { interface Props { onBack: () => void - onConfirm: (info: NewServerInfo) => void + onConfirm: (info: BaseServerInfo) => void title?: string - initialData?: NewServerInfo + initialData?: BaseServerInfo parseQs?: boolean - onQsConnect?: (server: NewServerInfo) => void - defaults?: Pick + onQsConnect?: (server: BaseServerInfo) => void + defaults?: Pick } export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQs, onQsConnect, defaults }: Props) => { @@ -33,7 +33,7 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ const [serverIp, setServerIp] = React.useState(ipWithoutPort ?? qsParams?.get('ip') ?? '') const [serverPort, setServerPort] = React.useState(port ?? '') - const [versionOverride, setVersionOverride] = React.useState(initialData?.versionOverride ?? qsParams?.get('version') ?? '') + const [versionOverride, setVersionOverride] = React.useState(initialData?.versionOverride ?? /* legacy */ initialData?.['version'] ?? qsParams?.get('version') ?? '') const [proxyOverride, setProxyOverride] = React.useState(initialData?.proxyOverride ?? qsParams?.get('proxy') ?? '') const [usernameOverride, setUsernameOverride] = React.useState(initialData?.usernameOverride ?? qsParams?.get('username') ?? '') const [passwordOverride, setPasswordOverride] = React.useState(initialData?.passwordOverride ?? qsParams?.get('password') ?? '') diff --git a/src/react/ServersList.tsx b/src/react/ServersList.tsx index 0972d3c6f..2fbbd3493 100644 --- a/src/react/ServersList.tsx +++ b/src/react/ServersList.tsx @@ -11,7 +11,7 @@ interface Props extends React.ComponentProps { username?: string password?: string proxy?: string - version?: string + versionOverride?: string shouldSave?: boolean }) => void initialProxies: SavedProxiesLocalStorage diff --git a/src/react/ServersListProvider.tsx b/src/react/ServersListProvider.tsx index 94d2f5cc0..2cad55d09 100644 --- a/src/react/ServersListProvider.tsx +++ b/src/react/ServersListProvider.tsx @@ -1,22 +1,16 @@ import { useEffect, useMemo, useState } from 'react' -import { proxy } from 'valtio' +import { proxy, useSnapshot } from 'valtio' import { qsOptions } from '../optionsStorage' import { ConnectOptions } from '../connect' import { hideCurrentModal, miscUiState, showModal } from '../globalState' import ServersList from './ServersList' -import AddServerOrConnect from './AddServerOrConnect' +import AddServerOrConnect, { BaseServerInfo } from './AddServerOrConnect' import { useDidUpdateEffect } from './utils' import { useIsModalActive } from './utilsApp' -interface StoreServerItem { - ip: string, - name?: string - version?: string +interface StoreServerItem extends BaseServerInfo { lastJoined?: number description?: string - proxyOverride?: string - usernameOverride?: string - passwordOverride?: string optionsOverride?: Record autoLogin?: Record } @@ -69,7 +63,7 @@ const getInitialServersList = () => { const legacyLastJoinedServer: StoreServerItem = { ip: localStorage['server'], passwordOverride: localStorage['password'], - version: localStorage['version'], + versionOverride: localStorage['version'], lastJoined: Date.now() } servers.push(legacyLastJoinedServer) @@ -80,7 +74,7 @@ const getInitialServersList = () => { servers.push({ ip: server.ip, description: server.description, - version: server.version, + versionOverride: server.version, }) } } @@ -206,7 +200,8 @@ const Inner = () => { setServersList(old => [...old, server]) } else { const index = serversList.indexOf(serverEditScreen) - serversList[index] = info + const { lastJoined } = serversList[index] + serversList[index] = { ...info, lastJoined } setServersList([...serversList]) } setServerEditScreen(null) @@ -248,7 +243,7 @@ const Inner = () => { username, server: ip, proxy: overrides.proxy || selectedProxy, - botVersion: overrides.version, + botVersion: overrides.versionOverride ?? /* legacy */ overrides['version'], password: overrides.password, ignoreQs: true, autoLoginPassword: server?.autoLogin?.[username], @@ -312,7 +307,7 @@ const Inner = () => { return { name: server.index.toString(), title: server.name || server.ip, - detail: (server.version ?? '') + ' ' + (server.usernameOverride ?? ''), + detail: (server.versionOverride ?? '') + ' ' + (server.usernameOverride ?? ''), // lastPlayed: server.lastJoined, formattedTextOverride: additional?.formattedText, worldNameRight: additional?.textNameRight ?? '', From 26dacc9c471bbbf7eb449a80209b619310ee039d Mon Sep 17 00:00:00 2001 From: Vitaly Date: Fri, 10 May 2024 05:06:05 +0300 Subject: [PATCH 4/8] restore full source map uploading to prod for better error stacks --- esbuild.mjs | 1 + scripts/esbuildPlugins.mjs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/esbuild.mjs b/esbuild.mjs index 3c35f32d2..4d423c87e 100644 --- a/esbuild.mjs +++ b/esbuild.mjs @@ -17,6 +17,7 @@ fs.writeFileSync('dist/index.html', fs.readFileSync('index.html', 'utf8').replac const watch = process.argv.includes('--watch') || process.argv.includes('-w') const prod = process.argv.includes('--prod') +if (prod) process.env.PROD = 'true' const dev = !prod const banner = [ diff --git a/scripts/esbuildPlugins.mjs b/scripts/esbuildPlugins.mjs index 8e0377072..999f9dc7e 100644 --- a/scripts/esbuildPlugins.mjs +++ b/scripts/esbuildPlugins.mjs @@ -116,7 +116,7 @@ const plugins = [ //@ts-ignore for (const file of outputFiles) { let contents = file.text - if (file.path.endsWith('.map') && file.text) { + if (file.path.endsWith('.map') && file.text && !process.env.PROD) { const map = JSON.parse(file.text) removeNodeModulesSourcemaps(map) contents = JSON.stringify(map) From 2e81fac75171b78a6b625a76d2f2bcf78899ee1f Mon Sep 17 00:00:00 2001 From: Vitaly Date: Fri, 10 May 2024 05:08:02 +0300 Subject: [PATCH 5/8] feat: implement full gamepad (joystics) support in all UIs including inventory! Add gamepad cursor, add pause menu bind --- package.json | 2 +- pnpm-lock.yaml | 19 ++---- src/controls.ts | 126 +++++++++++++++++++++++++++++++----- src/optionsStorage.ts | 4 +- src/react/Crosshair.css | 1 + src/react/SharedHudVars.tsx | 2 +- src/reactUi.tsx | 7 ++ src/styles.css | 12 ++++ 8 files changed, 142 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index 2e3b9dbaf..113245493 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,7 @@ "browserify-zlib": "^0.2.0", "buffer": "^6.0.3", "constants-browserify": "^1.0.0", - "contro-max": "^0.1.2", + "contro-max": "^0.1.6", "crypto-browserify": "^3.12.0", "cypress": "^10.11.0", "cypress-esbuild-preprocessor": "^1.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e0a424e96..bf324472f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -265,8 +265,8 @@ importers: specifier: ^1.0.0 version: 1.0.0 contro-max: - specifier: ^0.1.2 - version: 0.1.2(typescript@5.5.0-beta) + specifier: ^0.1.6 + version: 0.1.6(typescript@5.5.0-beta) crypto-browserify: specifier: ^3.12.0 version: 3.12.0 @@ -3808,8 +3808,8 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} - contro-max@0.1.2: - resolution: {integrity: sha512-mY9aRQ9on/iyzvyhb4OD/10WRRKulVd92F7cxMFVn3rq5EwI+gZitGpHN2mp9+IzwRgBJrOKr1C051b3YlEktQ==} + contro-max@0.1.6: + resolution: {integrity: sha512-QsoOcAlbtNgkCGBvwKsh+GUVZ2c5zfMgYQCu+v4MplX5VolkWhMwAcEOBRxt8oENbnRXOKUGQr816Ey1G4/jpg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} convert-source-map@1.9.0: @@ -4198,10 +4198,6 @@ packages: resolution: {integrity: sha512-y5JHnrygHnCndtqVHHDhCr0ZzzWHK5RBTczWRlGSIR5UnGHBXuxpoaE0UB5E82qym8ma2dI799wDSSJN2e4VSg==} engines: {node: '>=5'} - emittery@0.10.2: - resolution: {integrity: sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==} - engines: {node: '>=12'} - emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -12844,10 +12840,11 @@ snapshots: content-type@1.0.5: {} - contro-max@0.1.2(typescript@5.5.0-beta): + contro-max@0.1.6(typescript@5.5.0-beta): dependencies: - emittery: 0.10.2 + events: 3.3.0 lodash-es: 4.17.21 + typed-emitter: 2.1.0 optionalDependencies: react: 18.2.0 use-typed-event-listener: 4.0.2(react@18.2.0)(typescript@5.5.0-beta) @@ -13321,8 +13318,6 @@ snapshots: emit-then@2.0.0: {} - emittery@0.10.2: {} - emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} diff --git a/src/controls.ts b/src/controls.ts index f5e18ae71..6dd47be29 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -15,6 +15,7 @@ import { fsState } from './loadSave' import { showOptionsModal } from './react/SelectOption' import widgets from './react/widgets' import { getItemFromBlock } from './botUtils' +import { gamepadUiCursorState, moveGamepadCursorByPx } from './react/GamepadUiCursor' // todo move this to shared file with component export const customKeymaps = proxy(JSON.parse(localStorage.keymap || '{}')) @@ -45,7 +46,10 @@ export const contro = new ControMax({ }, ui: { back: [null/* 'Escape' */, 'B'], - click: [null, 'A'], + leftClick: [null, 'A'], + rightClick: [null, 'Y'], + speedupCursor: [null, 'Left Stick'], + pauseMenu: [null, 'Start'] }, advanced: { lockUrl: ['KeyY'], @@ -66,7 +70,7 @@ export const contro = new ControMax({ defaultControlOptions: controlOptions, target: document, captureEvents () { - return bot && isGameActive(false) + return true }, storeProvider: { load: () => customKeymaps, @@ -86,8 +90,18 @@ const setSprinting = (state: boolean) => { gameAdditionalState.isSprinting = state } -contro.on('movementUpdate', ({ vector, gamepadIndex }) => { +contro.on('movementUpdate', ({ vector, soleVector, gamepadIndex }) => { + if (gamepadIndex !== undefined && gamepadUiCursorState.display) { + const deadzone = 0.1 // TODO make deadzone configurable + if (Math.abs(soleVector.x) < deadzone && Math.abs(soleVector.z) < deadzone) { + return + } + moveGamepadCursorByPx(soleVector.x, true) + moveGamepadCursorByPx(soleVector.z, false) + emitMousemove() + } miscUiState.usingGamepadInput = gamepadIndex !== undefined + if (!bot || !isGameActive(false)) return // gamepadIndex will be used for splitscreen in future const coordToAction = [ ['z', -1, 'forward'], @@ -145,11 +159,71 @@ subscribe(activeModalStack, () => { } }) -const uiCommand = (command: Command) => { - if (command === 'ui.back') { - hideCurrentModal() - } else if (command === 'ui.click') { - // todo cursor +const emitMousemove = () => { + const { x, y } = gamepadUiCursorState + const xAbs = x / 100 * window.innerWidth + const yAbs = y / 100 * window.innerHeight + const element = document.elementFromPoint(xAbs, yAbs) as HTMLElement | null + if (!element) return + element.dispatchEvent(new MouseEvent('mousemove', { + clientX: xAbs, + clientY: yAbs + })) +} + +let lastClickedEl = null as HTMLElement | null +let lastClickedElTimeout: ReturnType | undefined +const inModalCommand = (command: Command, pressed: boolean) => { + if (pressed && !gamepadUiCursorState.display) return + + if (pressed) { + if (command === 'ui.back') { + hideCurrentModal() + } + if (command === 'ui.leftClick' || command === 'ui.rightClick') { + // in percent + const { x, y } = gamepadUiCursorState + const xAbs = x / 100 * window.innerWidth + const yAbs = y / 100 * window.innerHeight + const el = document.elementFromPoint(xAbs, yAbs) as HTMLElement + if (el) { + if (el === lastClickedEl && command === 'ui.leftClick') { + el.dispatchEvent(new MouseEvent('dblclick', { + bubbles: true, + clientX: xAbs, + clientY: yAbs + })) + return + } + el.dispatchEvent(new MouseEvent('mousedown', { + button: command === 'ui.leftClick' ? 0 : 2, + bubbles: true, + clientX: xAbs, + clientY: yAbs + })) + el.dispatchEvent(new MouseEvent(command === 'ui.leftClick' ? 'click' : 'contextmenu', { + bubbles: true, + clientX: xAbs, + clientY: yAbs + })) + el.dispatchEvent(new MouseEvent('mouseup', { + button: command === 'ui.leftClick' ? 0 : 2, + bubbles: true, + clientX: xAbs, + clientY: yAbs + })) + el.focus() + lastClickedEl = el + if (lastClickedElTimeout) clearTimeout(lastClickedElTimeout) + lastClickedElTimeout = setTimeout(() => { + lastClickedEl = null + }, 500) + } + } + } + + if (command === 'ui.speedupCursor') { + gamepadUiCursorState.multiply = pressed ? 2 : 1 } } @@ -160,10 +234,7 @@ const setSneaking = (state: boolean) => { const onTriggerOrReleased = (command: Command, pressed: boolean) => { // always allow release! - if (pressed && !isGameActive(true)) { - uiCommand(command) - return - } + if (!bot || !isGameActive(false)) return if (stringStartsWith(command, 'general')) { // handle general commands // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check @@ -199,7 +270,9 @@ const onTriggerOrReleased = (command: Command, pressed: boolean) => { } // im still not sure, maybe need to refactor to handle in inventory instead -const alwaysHandledCommand = (command: Command) => { +const alwaysPressedHandledCommand = (command: Command) => { + inModalCommand(command, true) + // triggered even outside of the game if (command === 'general.inventory') { if (activeModalStack.at(-1)?.reactType?.startsWith?.('player_win:')) { // todo? hideCurrentModal() @@ -207,9 +280,14 @@ const alwaysHandledCommand = (command: Command) => { } } +function cycleHotbarSlot (dir: 1 | -1) { + const newHotbarSlot = (bot.quickBarSlot + dir + 9) % 9 + bot.setQuickBarSlot(newHotbarSlot) +} + contro.on('trigger', ({ command }) => { const willContinue = !isGameActive(true) - alwaysHandledCommand(command) + alwaysPressedHandledCommand(command) if (willContinue) return const secondActionCommand = secondActionCommands[command] @@ -229,8 +307,15 @@ contro.on('trigger', ({ command }) => { onTriggerOrReleased(command, true) if (stringStartsWith(command, 'general')) { - // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check switch (command) { + case 'general.jump': + case 'general.sneak': + case 'general.toggleSneakOrDown': + case 'general.sprint': + case 'general.attackDestroy': + case 'general.interactPlace': + // handled in onTriggerOrReleased + break case 'general.inventory': document.exitPointerLock?.() openPlayerInventory() @@ -258,6 +343,12 @@ contro.on('trigger', ({ command }) => { case 'general.selectItem': void selectItem() break + case 'general.nextHotbarSlot': + cycleHotbarSlot(1) + break + case 'general.prevHotbarSlot': + cycleHotbarSlot(-1) + break } } if (command === 'advanced.lockUrl') { @@ -278,9 +369,14 @@ contro.on('trigger', ({ command }) => { window.history.replaceState({}, '', `${window.location.pathname}?${newQs}`) // return } + + if (command === 'ui.pauseMenu') { + showModal({ reactType: 'pause-screen' }) + } }) contro.on('release', ({ command }) => { + inModalCommand(command, false) onTriggerOrReleased(command, false) }) diff --git a/src/optionsStorage.ts b/src/optionsStorage.ts index 559740b61..27b7b5a68 100644 --- a/src/optionsStorage.ts +++ b/src/optionsStorage.ts @@ -6,8 +6,8 @@ import { subscribeKey } from 'valtio/utils' import { omitObj } from '@zardoy/utils' const defaultOptions = { - renderDistance: 2, - multiplayerRenderDistance: 2, + renderDistance: 3, + multiplayerRenderDistance: 3, closeConfirmation: true, autoFullScreen: false, mouseRawInput: false, diff --git a/src/react/Crosshair.css b/src/react/Crosshair.css index 94312669d..94252ffa7 100644 --- a/src/react/Crosshair.css +++ b/src/react/Crosshair.css @@ -8,4 +8,5 @@ top: 50%; left: 50%; transform: translate(-50%, -50%); + image-rendering: pixelated; } diff --git a/src/react/SharedHudVars.tsx b/src/react/SharedHudVars.tsx index ca48453a6..9a9392fd2 100644 --- a/src/react/SharedHudVars.tsx +++ b/src/react/SharedHudVars.tsx @@ -2,7 +2,7 @@ import { CSSProperties, useEffect } from 'react' import icons from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/icons.png' import widgets from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/widgets.png' -export default ({ children }) => { +export default ({ children }): React.ReactElement => { useEffect(() => { if (document.getElementById('hud-vars-style')) return // 1. Don't inline long data URLs for better DX in elements tab diff --git a/src/reactUi.tsx b/src/reactUi.tsx index 760a1a7b0..94ea6b885 100644 --- a/src/reactUi.tsx +++ b/src/reactUi.tsx @@ -35,6 +35,7 @@ import HotbarRenderApp from './react/HotbarRenderApp' import Crosshair from './react/Crosshair' import ButtonAppProvider from './react/ButtonAppProvider' import ServersListProvider from './react/ServersListProvider' +import GamepadUiCursor from './react/GamepadUiCursor' const RobustPortal = ({ children, to }) => { return createPortal({children}, to) @@ -149,6 +150,12 @@ const App = () => { {/* */} + +
+ +
+
+
} diff --git a/src/styles.css b/src/styles.css index 30abc52b4..fa57e51d9 100644 --- a/src/styles.css +++ b/src/styles.css @@ -134,6 +134,18 @@ body { -ms-interpolation-mode: nearest-neighbor; } +.overlay-top-scaled { + position: fixed; + inset: 0; + transform-origin: top left; + transform: scale(var(--guiScale)); + width: calc(100% / var(--guiScale)); + height: calc(100% / var(--guiScale)); + z-index: 80; + image-rendering: pixelated; + pointer-events: none; +} + #viewer-canvas { position: fixed; top: 0; From 620488af201a02fc38bc19a604876a26884c01a2 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Fri, 10 May 2024 05:08:28 +0300 Subject: [PATCH 6/8] Add Gamepad UI Cursor component and styles --- src/react/GamepadUiCursor.module.css | 11 ++++++++ src/react/GamepadUiCursor.tsx | 40 ++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 src/react/GamepadUiCursor.module.css create mode 100644 src/react/GamepadUiCursor.tsx diff --git a/src/react/GamepadUiCursor.module.css b/src/react/GamepadUiCursor.module.css new file mode 100644 index 000000000..f3f114e42 --- /dev/null +++ b/src/react/GamepadUiCursor.module.css @@ -0,0 +1,11 @@ +.crosshair { + width: 16px; + height: 16px; + background: var(--gui-icons); + background-size: calc(256px * var(--crosshair-scale)); + position: fixed; + z-index: 100; + transform: translate(-50%, -50%); + pointer-events: none; + image-rendering: pixelated; +} diff --git a/src/react/GamepadUiCursor.tsx b/src/react/GamepadUiCursor.tsx new file mode 100644 index 000000000..84c9b63a3 --- /dev/null +++ b/src/react/GamepadUiCursor.tsx @@ -0,0 +1,40 @@ +import { proxy, useSnapshot } from 'valtio' +import { useEffect } from 'react' +import { activeModalStack, miscUiState } from '../globalState' +import SharedHudVars from './SharedHudVars' +import styles from './GamepadUiCursor.module.css' + +export const gamepadUiCursorState = proxy({ + x: 50, + y: 50, + multiply: 1, + display: false +}) + +export const moveGamepadCursorByPx = (value: number, isX: boolean) => { + value *= gamepadUiCursorState.multiply * 3 + const valueToPercentage = value / (isX ? window.innerWidth : window.innerHeight) * 100 + gamepadUiCursorState[isX ? 'x' : 'y'] += valueToPercentage +} + +export default () => { + const hasModals = useSnapshot(activeModalStack).length > 0 + const { x, y } = useSnapshot(gamepadUiCursorState) + const { usingGamepadInput, gameLoaded } = useSnapshot(miscUiState) + + const doDisplay = usingGamepadInput && (hasModals || !gameLoaded) + + useEffect(() => { + document.body.style.cursor = gameLoaded && !hasModals && usingGamepadInput ? 'none' : 'auto' + }, [usingGamepadInput, hasModals, gameLoaded]) + + useEffect(() => { + gamepadUiCursorState.display = doDisplay + }, [doDisplay]) + + if (!doDisplay) return null + + return +
+ +} From e9443cd2fe0dece8749e291444473a89c6767459 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Thu, 9 May 2024 21:17:16 +0300 Subject: [PATCH 7/8] ui: fix input alignment --- src/react/input.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/input.module.css b/src/react/input.module.css index ce889645a..ce181ade4 100644 --- a/src/react/input.module.css +++ b/src/react/input.module.css @@ -8,7 +8,7 @@ } .input { - position: relative; + position: absolute; outline: none; border: none; background: none; From 3f068ed0da7ebb4cd6519c0d473683cb86ebd28d Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Fri, 10 May 2024 06:03:25 +0300 Subject: [PATCH 8/8] fix tsc --- src/react/TouchAreasControls.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/react/TouchAreasControls.tsx b/src/react/TouchAreasControls.tsx index f56db0471..b660fcb26 100644 --- a/src/react/TouchAreasControls.tsx +++ b/src/react/TouchAreasControls.tsx @@ -40,12 +40,14 @@ export const handleMovementStickDelta = (e?: { clientX, clientY }) => { } joystickPointer.joystickInner!.style.transform = `translate(${x}px, ${y}px)` + const vector = { + x: x / max, + y: 0, + z: y / max, + } void contro.emit('movementUpdate', { - vector: { - x: x / max, - y: 0, - z: y / max, - }, + vector, + soleVector: vector }) }