diff --git a/README.MD b/README.MD index aa36f7e8..4769192a 100644 --- a/README.MD +++ b/README.MD @@ -34,6 +34,7 @@ All components that are in [Storybook](https://mcraft.fun/storybook) are publish - Controls -> **Touch Controls Type** -> **Joystick** - Controls -> **Auto Full Screen** -> **On** - To avoid ctrl+w issue +- Interface -> **Enable Minimap** -> **Always** - To enable useful minimap (why not?) - Controls -> **Raw Input** -> **On** - This will make the controls more precise (UPD: already enabled by default) - Interface -> **Chat Select** -> **On** - To select chat messages (UPD: already enabled by default) diff --git a/config.json b/config.json index ea27ca5a..bdfd28b2 100644 --- a/config.json +++ b/config.json @@ -26,7 +26,8 @@ } ], "rightSideText": "A Minecraft client clone in the browser!", - "splashText": "Gen is cooking!", + "splashText": "The sunset is coming!", + "splashTextFallback": "Welcome!", "pauseLinks": [ [ { @@ -36,5 +37,38 @@ "type": "discord" } ] + ], + "mobileButtons": [ + { + "action": "general.drop", + "actionHold": "general.dropStack", + "label": "Q" + }, + { + "action": "general.selectItem", + "actionHold": "", + "label": "S" + }, + { + "action": "general.debugOverlay", + "actionHold": "general.debugOverlayHelpMenu", + "label": "F3" + }, + { + "action": "general.playersList", + "actionHold": "", + "icon": "pixelarticons:users", + "label": "TAB" + }, + { + "action": "general.chat", + "actionHold": "", + "label": "" + }, + { + "action": "ui.pauseMenu", + "actionHold": "", + "label": "" + } ] } diff --git a/package.json b/package.json index 16f009f0..655cd394 100644 --- a/package.json +++ b/package.json @@ -51,8 +51,8 @@ "dependencies": { "@dimaka/interface": "0.0.3-alpha.0", "@floating-ui/react": "^0.26.1", - "@nxg-org/mineflayer-auto-jump": "^0.7.12", - "@nxg-org/mineflayer-tracker": "1.2.1", + "@nxg-org/mineflayer-auto-jump": "^0.7.18", + "@nxg-org/mineflayer-tracker": "1.3.0", "@react-oauth/google": "^0.12.1", "@stylistic/eslint-plugin": "^2.6.1", "@types/gapi": "^0.0.47", @@ -83,7 +83,7 @@ "jszip": "^3.10.1", "lodash-es": "^4.17.21", "mcraft-fun-mineflayer": "^0.1.23", - "minecraft-data": "3.83.1", + "minecraft-data": "3.89.0", "minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master", "mineflayer-item-map-downloader": "github:zardoy/mineflayer-item-map-downloader", "mojangson": "^2.0.4", @@ -140,7 +140,7 @@ "browserify-zlib": "^0.2.0", "buffer": "^6.0.3", "constants-browserify": "^1.0.0", - "contro-max": "^0.1.8", + "contro-max": "^0.1.9", "crypto-browserify": "^3.12.0", "cypress-esbuild-preprocessor": "^1.0.2", "eslint": "^8.50.0", @@ -150,7 +150,7 @@ "http-browserify": "^1.7.0", "http-server": "^14.1.1", "https-browserify": "^1.0.0", - "mc-assets": "^0.2.53", + "mc-assets": "^0.2.54", "minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next", "mineflayer": "github:zardoy/mineflayer#gen-the-master", "mineflayer-mouse": "^0.1.10", @@ -194,14 +194,14 @@ }, "pnpm": { "overrides": { - "@nxg-org/mineflayer-physics-util": "1.8.7", + "@nxg-org/mineflayer-physics-util": "1.8.10", "buffer": "^6.0.3", "vec3": "0.1.10", "three": "0.154.0", "diamond-square": "github:zardoy/diamond-square", "prismarine-block": "github:zardoy/prismarine-block#next-era", "prismarine-world": "github:zardoy/prismarine-world#next-era", - "minecraft-data": "3.83.1", + "minecraft-data": "3.89.0", "prismarine-provider-anvil": "github:zardoy/prismarine-provider-anvil#everything", "prismarine-physics": "github:zardoy/prismarine-physics", "minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master", diff --git a/patches/minecraft-protocol.patch b/patches/minecraft-protocol.patch index 29111f69..efc79176 100644 --- a/patches/minecraft-protocol.patch +++ b/patches/minecraft-protocol.patch @@ -1,3 +1,6 @@ +diff --git a/README.md b/README.md +deleted file mode 100644 +index fbcaa43667323a58b8110a4495938c2c6d2d6f83..0000000000000000000000000000000000000000 diff --git a/src/client/chat.js b/src/client/chat.js index f14269bea055d4329cd729271e7406ec4b344de7..00f5482eb6e3c911381ca9a728b1b4aae0d1d337 100644 --- a/src/client/chat.js @@ -73,6 +76,19 @@ index b9d21bab9faccd5dbf1975fc423fc55c73e906c5..99ffd76527b410e3a393181beb260108 } function onJoinServerResponse (err) { +diff --git a/src/client/play.js b/src/client/play.js +index 6e06dc15291b38e1eeeec8d7102187b2a23d70a3..f67454942db9276cbb9eab99c281cfe182cb8a1f 100644 +--- a/src/client/play.js ++++ b/src/client/play.js +@@ -53,7 +53,7 @@ module.exports = function (client, options) { + client.write('configuration_acknowledged', {}) + } + client.state = states.CONFIGURATION +- client.on('select_known_packs', () => { ++ client.once('select_known_packs', () => { + client.write('select_known_packs', { packs: [] }) + }) + // Server should send finish_configuration on its own right after sending the client a dimension codec diff --git a/src/client.js b/src/client.js index 74749698f8cee05b5dc749c271544f78d06645b0..e77e0a3f41c1ee780c3abbd54b0801d248c2a07c 100644 --- a/src/client.js diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f099a85f..60086040 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,14 +5,14 @@ settings: excludeLinksFromLockfile: false overrides: - '@nxg-org/mineflayer-physics-util': 1.8.7 + '@nxg-org/mineflayer-physics-util': 1.8.10 buffer: ^6.0.3 vec3: 0.1.10 three: 0.154.0 diamond-square: github:zardoy/diamond-square prismarine-block: github:zardoy/prismarine-block#next-era prismarine-world: github:zardoy/prismarine-world#next-era - minecraft-data: 3.83.1 + minecraft-data: 3.89.0 prismarine-provider-anvil: github:zardoy/prismarine-provider-anvil#everything prismarine-physics: github:zardoy/prismarine-physics minecraft-protocol: github:PrismarineJS/node-minecraft-protocol#master @@ -22,7 +22,7 @@ overrides: patchedDependencies: minecraft-protocol: - hash: 3a55a278c417cc34ff3172cd1de8e22852935cba0586875cbd0635f1ffdaa5ab + hash: 1546deaf50efae3d6564fcc9f08da99d3ae8096ac98f420b87b48b8c9501fd37 path: patches/minecraft-protocol.patch mineflayer-item-map-downloader@1.2.0: hash: a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad @@ -42,11 +42,11 @@ importers: specifier: ^0.26.1 version: 0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@nxg-org/mineflayer-auto-jump': - specifier: ^0.7.12 - version: 0.7.12 + specifier: ^0.7.18 + version: 0.7.18 '@nxg-org/mineflayer-tracker': - specifier: 1.2.1 - version: 1.2.1(encoding@0.1.13) + specifier: 1.3.0 + version: 1.3.0(encoding@0.1.13) '@react-oauth/google': specifier: ^0.12.1 version: 0.12.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -136,13 +136,13 @@ importers: version: 4.17.21 mcraft-fun-mineflayer: specifier: ^0.1.23 - version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/5602eca4174c0aff079e60234d7c68327eeb6fe6(encoding@0.1.13)) + version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/e4818de9e1ba2783a930b4358499ac4033e6383d(encoding@0.1.13)) minecraft-data: - specifier: 3.83.1 - version: 3.83.1 + specifier: 3.89.0 + version: 3.89.0 minecraft-protocol: specifier: github:PrismarineJS/node-minecraft-protocol#master - version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=3a55a278c417cc34ff3172cd1de8e22852935cba0586875cbd0635f1ffdaa5ab)(encoding@0.1.13) + version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=1546deaf50efae3d6564fcc9f08da99d3ae8096ac98f420b87b48b8c9501fd37)(encoding@0.1.13) mineflayer-item-map-downloader: specifier: github:zardoy/mineflayer-item-map-downloader version: https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad)(encoding@0.1.13) @@ -166,7 +166,7 @@ importers: version: 6.1.1 prismarine-provider-anvil: specifier: github:zardoy/prismarine-provider-anvil#everything - version: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.83.1) + version: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.89.0) prosemirror-example-setup: specifier: ^1.2.2 version: 1.2.3 @@ -304,8 +304,8 @@ importers: specifier: ^1.0.0 version: 1.0.0 contro-max: - specifier: ^0.1.8 - version: 0.1.8(typescript@5.5.4) + specifier: ^0.1.9 + version: 0.1.9(typescript@5.5.4) crypto-browserify: specifier: ^3.12.0 version: 3.12.1 @@ -334,14 +334,14 @@ importers: specifier: ^1.0.0 version: 1.0.0 mc-assets: - specifier: ^0.2.53 - version: 0.2.53 + specifier: ^0.2.54 + version: 0.2.54 minecraft-inventory-gui: specifier: github:zardoy/minecraft-inventory-gui#next - version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/98bc5bb8ee6da8b4b771c05b404cee796318ccd4(@types/react@18.3.18)(react@18.3.1) + version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/f425e6a2db796d42cd2c09cf715dad841dec643d(@types/react@18.3.18)(react@18.3.1) mineflayer: specifier: github:zardoy/mineflayer#gen-the-master - version: https://codeload.github.com/zardoy/mineflayer/tar.gz/5602eca4174c0aff079e60234d7c68327eeb6fe6(encoding@0.1.13) + version: https://codeload.github.com/zardoy/mineflayer/tar.gz/e4818de9e1ba2783a930b4358499ac4033e6383d(encoding@0.1.13) mineflayer-mouse: specifier: ^0.1.10 version: 0.1.10(@types/debug@4.1.12)(@types/node@22.13.9)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) @@ -435,7 +435,7 @@ importers: version: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chunk: specifier: github:zardoy/prismarine-chunk#master - version: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.83.1) + version: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.89.0) prismarine-schematic: specifier: ^1.2.0 version: 1.2.3 @@ -2019,14 +2019,14 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} deprecated: This functionality has been moved to @npmcli/fs - '@nxg-org/mineflayer-auto-jump@0.7.12': - resolution: {integrity: sha512-F5vX/lerlWx/5HVlkDNbvrtQ19PL6iG8i4ItPTIRtjGiFzusDefP7DI226zSFR8Wlaw45qHv0jn814p/4/qVdQ==} + '@nxg-org/mineflayer-auto-jump@0.7.18': + resolution: {integrity: sha512-O/nRCyWrRwFpcCXXSJhmt844c4a8KhkK4OJPAOKSc63tExIIQU/sipHgjgpy0B+gVDjSmLMPYXe71CN0W327Wg==} - '@nxg-org/mineflayer-physics-util@1.8.7': - resolution: {integrity: sha512-wtLYvHqoEFr/j0ny2lyogwjbMvwpFuG2aWI8sI14+EAiGFRpL5+cog2ujSDsnRTZruO7tUXMTiPc1kebjXwfJg==} + '@nxg-org/mineflayer-physics-util@1.8.10': + resolution: {integrity: sha512-JGIJEPauVmqoBFQ0I8ZtbaYo3mKn2N00srnDrWkCEt1qozyZWie4sYR0khjjwYubFCljMoWtoEA0+DLsHZLNFg==} - '@nxg-org/mineflayer-tracker@1.2.1': - resolution: {integrity: sha512-SI1ffF8zvg3/ZNE021Ja2W0FZPN+WbQDZf8yFqOcXtPRXAtM9W6HvoACdzXep8BZid7WYgYLIgjKpB+9RqvCNQ==} + '@nxg-org/mineflayer-tracker@1.3.0': + resolution: {integrity: sha512-HINrv51l2aZ/lDrcL77gSWDvf3Z3trd6kdiifXitCMDNdBT0FpWnXq9bi5Fr7yPpFGQ3fqGUIq5DQYYY84E9IA==} '@nxg-org/mineflayer-trajectories@1.2.0': resolution: {integrity: sha512-yTDHn96fyWLKwdHdOGIrnt8nss4SJmxXwJn101o7aNI4sgdnUmwaX4FoNbmrEa9eZn6IwxaXIxDf+fJmKj9RIw==} @@ -4237,8 +4237,8 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} - contro-max@0.1.8: - resolution: {integrity: sha512-5SoeudO8Zzfj/gbFTDrMRFJny02+MY1lBtb2NyCNiBLtHAfvhWZxZs/Z3yJvKL2rY/qKUZs9gTQOIDygBcBrdw==} + contro-max@0.1.9: + resolution: {integrity: sha512-zH9FB60EzhHKublD92d11QuarYRTdYci5rvDgwDr5XXwUqae5mr6IgzXGcr78T2odnO/Aeqmrf32RDwJIl5GfQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} convert-source-map@1.9.0: @@ -4428,6 +4428,15 @@ packages: supports-color: optional: true + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} engines: {node: '>=0.10.0'} @@ -6466,8 +6475,8 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - mc-assets@0.2.53: - resolution: {integrity: sha512-Ucsu2pDLr/cs8bxbxU9KTszdf/vPTLphYgEHUEWxuYlMkPQUCpsQwkn3YgyykJ7RXaca7zZGlZXaTPXBAqJT6A==} + mc-assets@0.2.54: + resolution: {integrity: sha512-ZEaa9IcqfOt4cFGLVJVkZMemKPfbjQskvIxuDepkXWkJb9T+xQ+Hj86zDMh1Ah8WZWNeGx2x26CuXt8QNr6pcw==} engines: {node: '>=18.0.0'} mcraft-fun-mineflayer@0.1.23: @@ -6678,14 +6687,14 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} - minecraft-data@3.83.1: - resolution: {integrity: sha512-5K26za9k5WV1OnfkGexA77lBhfGZeFw3rT3NM7/rbFXRZC65prCx7Tk2BQvC9UfzgxxvmxHfxM5y8G1U+Oxgfg==} + minecraft-data@3.89.0: + resolution: {integrity: sha512-v6dUr1M7Pjc6N4ujanrBZu3IP4/HbSBpxSSXNbK6HVFVJqfaqKSMXN57G/JAlDcwqXYsVd9H4tbKFHCO+VmQpg==} minecraft-folder-path@1.2.0: resolution: {integrity: sha512-qaUSbKWoOsH9brn0JQuBhxNAzTDMwrOXorwuRxdJKKKDYvZhtml+6GVCUrY5HRiEsieBEjCUnhVpDuQiKsiFaw==} - minecraft-inventory-gui@https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/98bc5bb8ee6da8b4b771c05b404cee796318ccd4: - resolution: {tarball: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/98bc5bb8ee6da8b4b771c05b404cee796318ccd4} + minecraft-inventory-gui@https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/f425e6a2db796d42cd2c09cf715dad841dec643d: + resolution: {tarball: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/f425e6a2db796d42cd2c09cf715dad841dec643d} version: 1.0.1 minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284: @@ -6715,8 +6724,8 @@ packages: resolution: {integrity: sha512-3bxph4jfbkBh5HpeouorxzrfSLNV+i+1gugNJ2jf52HW+rt+tW7eiiFPxrJEsOVkPT/3O/dEIW7j93LRlojMkQ==} engines: {node: '>=22'} - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/5602eca4174c0aff079e60234d7c68327eeb6fe6: - resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/5602eca4174c0aff079e60234d7c68327eeb6fe6} + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/e4818de9e1ba2783a930b4358499ac4033e6383d: + resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/e4818de9e1ba2783a930b4358499ac4033e6383d} version: 4.27.0 engines: {node: '>=22'} @@ -7415,7 +7424,7 @@ packages: prismarine-biome@1.3.0: resolution: {integrity: sha512-GY6nZxq93mTErT7jD7jt8YS1aPrOakbJHh39seYsJFXvueIOdHAmW16kYQVrTVMW5MlWLQVxV/EquRwOgr4MnQ==} peerDependencies: - minecraft-data: 3.83.1 + minecraft-data: 3.89.0 prismarine-registry: ^1.1.0 prismarine-block@https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9: @@ -9742,7 +9751,7 @@ snapshots: '@babel/core': 7.26.9 '@babel/helper-compilation-targets': 7.26.5 '@babel/helper-plugin-utils': 7.26.5 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 lodash.debounce: 4.0.8 resolve: 1.22.10 transitivePeerDependencies: @@ -11368,17 +11377,18 @@ snapshots: rimraf: 3.0.2 optional: true - '@nxg-org/mineflayer-auto-jump@0.7.12': + '@nxg-org/mineflayer-auto-jump@0.7.18': dependencies: - '@nxg-org/mineflayer-physics-util': 1.8.7 + '@nxg-org/mineflayer-physics-util': 1.8.10 strict-event-emitter-types: 2.0.0 - '@nxg-org/mineflayer-physics-util@1.8.7': + '@nxg-org/mineflayer-physics-util@1.8.10': dependencies: '@nxg-org/mineflayer-util-plugin': 1.8.4 - '@nxg-org/mineflayer-tracker@1.2.1(encoding@0.1.13)': + '@nxg-org/mineflayer-tracker@1.3.0(encoding@0.1.13)': dependencies: + '@nxg-org/mineflayer-physics-util': 1.8.10 '@nxg-org/mineflayer-trajectories': 1.2.0(encoding@0.1.13) '@nxg-org/mineflayer-util-plugin': 1.8.4 transitivePeerDependencies: @@ -11388,7 +11398,7 @@ snapshots: '@nxg-org/mineflayer-trajectories@1.2.0(encoding@0.1.13)': dependencies: '@nxg-org/mineflayer-util-plugin': 1.8.4 - minecraft-data: 3.83.1 + minecraft-data: 3.89.0 mineflayer: 4.27.0(encoding@0.1.13) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-item: 1.16.0 @@ -13189,16 +13199,16 @@ snapshots: exit-hook: 2.2.1 flatmap: 0.0.3 long: 5.3.1 - minecraft-data: 3.83.1 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=3a55a278c417cc34ff3172cd1de8e22852935cba0586875cbd0635f1ffdaa5ab)(encoding@0.1.13) + minecraft-data: 3.89.0 + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=1546deaf50efae3d6564fcc9f08da99d3ae8096ac98f420b87b48b8c9501fd37)(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 - prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.83.1) + prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.89.0) prismarine-entity: 2.5.0 prismarine-item: 1.16.0 prismarine-nbt: 2.7.0 - prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.83.1) + prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.89.0) prismarine-windows: 2.9.0 prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c rambda: 9.4.2 @@ -13225,16 +13235,16 @@ snapshots: exit-hook: 2.2.1 flatmap: 0.0.3 long: 5.3.1 - minecraft-data: 3.83.1 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=3a55a278c417cc34ff3172cd1de8e22852935cba0586875cbd0635f1ffdaa5ab)(encoding@0.1.13) + minecraft-data: 3.89.0 + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=1546deaf50efae3d6564fcc9f08da99d3ae8096ac98f420b87b48b8c9501fd37)(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 - prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.83.1) + prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.89.0) prismarine-entity: 2.5.0 prismarine-item: 1.16.0 prismarine-nbt: 2.7.0 - prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.83.1) + prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.89.0) prismarine-windows: 2.9.0 prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c rambda: 9.4.2 @@ -13312,7 +13322,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 transitivePeerDependencies: - supports-color optional: true @@ -14215,7 +14225,7 @@ snapshots: content-type@1.0.5: {} - contro-max@0.1.8(typescript@5.5.4): + contro-max@0.1.9(typescript@5.5.4): dependencies: events: 3.3.0 lodash-es: 4.17.21 @@ -14480,6 +14490,10 @@ snapshots: optionalDependencies: supports-color: 8.1.1 + debug@4.4.1: + dependencies: + ms: 2.1.3 + decamelize-keys@1.1.1: dependencies: decamelize: 1.2.0 @@ -14624,8 +14638,8 @@ snapshots: diamond-square@https://codeload.github.com/zardoy/diamond-square/tar.gz/cfaad2d1d5909fdfa63c8cc7bc05fb5e87782d71: dependencies: - minecraft-data: 3.83.1 - prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.83.1) + minecraft-data: 3.89.0 + prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.89.0) prismarine-registry: 1.11.0 random-seed: 0.3.0 vec3: 0.1.10 @@ -16128,7 +16142,7 @@ snapshots: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 transitivePeerDependencies: - supports-color optional: true @@ -17014,17 +17028,17 @@ snapshots: math-intrinsics@1.1.0: {} - mc-assets@0.2.53: + mc-assets@0.2.54: dependencies: maxrects-packer: '@zardoy/maxrects-packer@2.7.4' zod: 3.24.2 - mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/5602eca4174c0aff079e60234d7c68327eeb6fe6(encoding@0.1.13)): + mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/e4818de9e1ba2783a930b4358499ac4033e6383d(encoding@0.1.13)): dependencies: '@zardoy/flying-squid': 0.0.49(encoding@0.1.13) exit-hook: 2.2.1 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=3a55a278c417cc34ff3172cd1de8e22852935cba0586875cbd0635f1ffdaa5ab)(encoding@0.1.13) - mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/5602eca4174c0aff079e60234d7c68327eeb6fe6(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=1546deaf50efae3d6564fcc9f08da99d3ae8096ac98f420b87b48b8c9501fd37)(encoding@0.1.13) + mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/e4818de9e1ba2783a930b4358499ac4033e6383d(encoding@0.1.13) prismarine-item: 1.16.0 ws: 8.18.1 transitivePeerDependencies: @@ -17254,7 +17268,7 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.12 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 decode-named-character-reference: 1.1.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -17331,18 +17345,18 @@ snapshots: min-indent@1.0.1: {} - minecraft-data@3.83.1: {} + minecraft-data@3.89.0: {} minecraft-folder-path@1.2.0: {} - minecraft-inventory-gui@https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/98bc5bb8ee6da8b4b771c05b404cee796318ccd4(@types/react@18.3.18)(react@18.3.1): + minecraft-inventory-gui@https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/f425e6a2db796d42cd2c09cf715dad841dec643d(@types/react@18.3.18)(react@18.3.1): dependencies: valtio: 1.13.2(@types/react@18.3.18)(react@18.3.1) transitivePeerDependencies: - '@types/react' - react - minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=3a55a278c417cc34ff3172cd1de8e22852935cba0586875cbd0635f1ffdaa5ab)(encoding@0.1.13): + minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=1546deaf50efae3d6564fcc9f08da99d3ae8096ac98f420b87b48b8c9501fd37)(encoding@0.1.13): dependencies: '@types/node-rsa': 1.1.4 '@types/readable-stream': 4.0.18 @@ -17351,7 +17365,7 @@ snapshots: debug: 4.4.0(supports-color@8.1.1) endian-toggle: 0.0.0 lodash.merge: 4.6.2 - minecraft-data: 3.83.1 + minecraft-data: 3.89.0 minecraft-folder-path: 1.2.0 node-fetch: 2.7.0(encoding@0.1.13) node-rsa: 0.4.2 @@ -17430,7 +17444,7 @@ snapshots: mineflayer-pathfinder@2.4.5: dependencies: - minecraft-data: 3.83.1 + minecraft-data: 3.89.0 prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-entity: 2.5.0 prismarine-item: 1.16.0 @@ -17440,12 +17454,12 @@ snapshots: mineflayer@4.27.0(encoding@0.1.13): dependencies: - minecraft-data: 3.83.1 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=3a55a278c417cc34ff3172cd1de8e22852935cba0586875cbd0635f1ffdaa5ab)(encoding@0.1.13) - prismarine-biome: 1.3.0(minecraft-data@3.83.1)(prismarine-registry@1.11.0) + minecraft-data: 3.89.0 + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=1546deaf50efae3d6564fcc9f08da99d3ae8096ac98f420b87b48b8c9501fd37)(encoding@0.1.13) + prismarine-biome: 1.3.0(minecraft-data@3.89.0)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chat: 1.11.0 - prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.83.1) + prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.89.0) prismarine-entity: 2.5.0 prismarine-item: 1.16.0 prismarine-nbt: 2.7.0 @@ -17461,15 +17475,15 @@ snapshots: - encoding - supports-color - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/5602eca4174c0aff079e60234d7c68327eeb6fe6(encoding@0.1.13): + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/e4818de9e1ba2783a930b4358499ac4033e6383d(encoding@0.1.13): dependencies: - '@nxg-org/mineflayer-physics-util': 1.8.7 - minecraft-data: 3.83.1 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=3a55a278c417cc34ff3172cd1de8e22852935cba0586875cbd0635f1ffdaa5ab)(encoding@0.1.13) - prismarine-biome: 1.3.0(minecraft-data@3.83.1)(prismarine-registry@1.11.0) + '@nxg-org/mineflayer-physics-util': 1.8.10 + minecraft-data: 3.89.0 + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(patch_hash=1546deaf50efae3d6564fcc9f08da99d3ae8096ac98f420b87b48b8c9501fd37)(encoding@0.1.13) + prismarine-biome: 1.3.0(minecraft-data@3.89.0)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-chat: 1.11.0 - prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.83.1) + prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.89.0) prismarine-entity: 2.5.0 prismarine-item: 1.16.0 prismarine-nbt: 2.7.0 @@ -18255,15 +18269,15 @@ snapshots: transitivePeerDependencies: - supports-color - prismarine-biome@1.3.0(minecraft-data@3.83.1)(prismarine-registry@1.11.0): + prismarine-biome@1.3.0(minecraft-data@3.89.0)(prismarine-registry@1.11.0): dependencies: - minecraft-data: 3.83.1 + minecraft-data: 3.89.0 prismarine-registry: 1.11.0 prismarine-block@https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9: dependencies: - minecraft-data: 3.83.1 - prismarine-biome: 1.3.0(minecraft-data@3.83.1)(prismarine-registry@1.11.0) + minecraft-data: 3.89.0 + prismarine-biome: 1.3.0(minecraft-data@3.89.0)(prismarine-registry@1.11.0) prismarine-chat: 1.11.0 prismarine-item: 1.16.0 prismarine-nbt: 2.7.0 @@ -18275,9 +18289,9 @@ snapshots: prismarine-nbt: 2.7.0 prismarine-registry: 1.11.0 - prismarine-chunk@https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.83.1): + prismarine-chunk@https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.89.0): dependencies: - prismarine-biome: 1.3.0(minecraft-data@3.83.1)(prismarine-registry@1.11.0) + prismarine-biome: 1.3.0(minecraft-data@3.89.0)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-nbt: 2.7.0 prismarine-registry: 1.11.0 @@ -18306,14 +18320,14 @@ snapshots: prismarine-physics@https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b: dependencies: - minecraft-data: 3.83.1 + minecraft-data: 3.89.0 prismarine-nbt: 2.7.0 vec3: 0.1.10 - prismarine-provider-anvil@https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.83.1): + prismarine-provider-anvil@https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.89.0): dependencies: prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 - prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.83.1) + prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.89.0) prismarine-nbt: 2.7.0 prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c uint4: 0.1.2 @@ -18335,13 +18349,13 @@ snapshots: prismarine-registry@1.11.0: dependencies: - minecraft-data: 3.83.1 + minecraft-data: 3.89.0 prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-nbt: 2.7.0 prismarine-schematic@1.2.3: dependencies: - minecraft-data: 3.83.1 + minecraft-data: 3.89.0 prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-nbt: 2.7.0 prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c @@ -19180,7 +19194,7 @@ snapshots: send@1.2.0: dependencies: - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -19535,7 +19549,7 @@ snapshots: socks-proxy-agent@7.0.0: dependencies: agent-base: 6.0.2 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 socks: 2.8.4 transitivePeerDependencies: - supports-color @@ -20448,7 +20462,7 @@ snapshots: vite-node@3.0.8(@types/node@22.13.9)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0): dependencies: cac: 6.7.14 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 es-module-lexer: 1.6.0 pathe: 2.0.3 vite: 6.2.1(@types/node@22.13.9)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) @@ -20534,7 +20548,7 @@ snapshots: '@vitest/spy': 3.0.8 '@vitest/utils': 3.0.8 chai: 5.2.0 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 expect-type: 1.2.0 magic-string: 0.30.17 pathe: 2.0.3 diff --git a/renderer/viewer/baseGraphicsBackend.ts b/renderer/viewer/baseGraphicsBackend.ts index 79607695..4724076a 100644 --- a/renderer/viewer/baseGraphicsBackend.ts +++ b/renderer/viewer/baseGraphicsBackend.ts @@ -3,7 +3,8 @@ import { RendererReactiveState } from '../../src/appViewer' export const getDefaultRendererState = (): RendererReactiveState => { return { world: { - chunksLoaded: [], + chunksLoaded: new Set(), + heightmaps: new Map(), chunksTotalNumber: 0, allChunksLoaded: true, mesherWork: false, diff --git a/renderer/viewer/lib/basePlayerState.ts b/renderer/viewer/lib/basePlayerState.ts index df8cbc61..856772b1 100644 --- a/renderer/viewer/lib/basePlayerState.ts +++ b/renderer/viewer/lib/basePlayerState.ts @@ -31,6 +31,8 @@ export interface IPlayerState { getHeldItem?(isLeftHand: boolean): HandItemBlock | undefined username?: string onlineMode?: boolean + lightingDisabled?: boolean + shouldHideHand?: boolean events: TypedEmitter diff --git a/renderer/viewer/lib/mesher/mesher.ts b/renderer/viewer/lib/mesher/mesher.ts index 21e2d8ef..f26d8022 100644 --- a/renderer/viewer/lib/mesher/mesher.ts +++ b/renderer/viewer/lib/mesher/mesher.ts @@ -2,6 +2,7 @@ import { Vec3 } from 'vec3' import { World } from './world' import { getSectionGeometry, setBlockStatesData as setMesherData } from './models' import { BlockStateModelInfo } from './shared' +import { INVISIBLE_BLOCKS } from './worldConstants' globalThis.structuredClone ??= (value) => JSON.parse(JSON.stringify(value)) @@ -148,6 +149,30 @@ const handleMessage = data => { global.postMessage({ type: 'customBlockModel', chunkKey, customBlockModel }) break } + case 'getHeightmap': { + const heightmap = new Uint8Array(256) + + const blockPos = new Vec3(0, 0, 0) + for (let z = 0; z < 16; z++) { + for (let x = 0; x < 16; x++) { + const blockX = x + data.x + const blockZ = z + data.z + blockPos.x = blockX + blockPos.z = blockZ + blockPos.y = world.config.worldMaxY + let block = world.getBlock(blockPos) + while (block && INVISIBLE_BLOCKS.has(block.name) && blockPos.y > world.config.worldMinY) { + blockPos.y -= 1 + block = world.getBlock(blockPos) + } + const index = z * 16 + x + heightmap[index] = block ? blockPos.y : 0 + } + } + postMessage({ type: 'heightmap', key: `${Math.floor(data.x / 16)},${Math.floor(data.z / 16)}`, heightmap }) + + break + } // No default } } diff --git a/renderer/viewer/lib/mesher/models.ts b/renderer/viewer/lib/mesher/models.ts index 46258134..3658d120 100644 --- a/renderer/viewer/lib/mesher/models.ts +++ b/renderer/viewer/lib/mesher/models.ts @@ -125,6 +125,13 @@ const isCube = (block: Block) => { })) } +const getVec = (v: Vec3, dir: Vec3) => { + for (const coord of ['x', 'y', 'z']) { + if (Math.abs(dir[coord]) > 0) v[coord] = 0 + } + return v.plus(dir) +} + function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, type: number, biome: string, water: boolean, attr: Record, isRealWater: boolean) { const heights: number[] = [] for (let z = -1; z <= 1; z++) { @@ -142,7 +149,7 @@ function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, typ // eslint-disable-next-line guard-for-in for (const face in elemFaces) { - const { dir, corners } = elemFaces[face] + const { dir, corners, mask1, mask2 } = elemFaces[face] const isUp = dir[1] === 1 const neighborPos = cursor.offset(...dir as [number, number, number]) @@ -180,6 +187,9 @@ function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, typ const { su } = texture const { sv } = texture + // Get base light value for the face + const baseLight = world.getLight(neighborPos, undefined, undefined, water ? 'water' : 'lava') / 15 + for (const pos of corners) { const height = cornerHeights[pos[2] * 2 + pos[0]] attr.t_positions.push( @@ -189,7 +199,31 @@ function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, typ ) attr.t_normals.push(...dir) attr.t_uvs.push(pos[3] * su + u, pos[4] * sv * (pos[1] ? 1 : height) + v) - attr.t_colors.push(tint[0], tint[1], tint[2]) + + let cornerLightResult = baseLight + if (world.config.smoothLighting) { + const dx = pos[0] * 2 - 1 + const dy = pos[1] * 2 - 1 + const dz = pos[2] * 2 - 1 + const cornerDir: [number, number, number] = [dx, dy, dz] + const side1Dir: [number, number, number] = [dx * mask1[0], dy * mask1[1], dz * mask1[2]] + const side2Dir: [number, number, number] = [dx * mask2[0], dy * mask2[1], dz * mask2[2]] + + const dirVec = new Vec3(...dir as [number, number, number]) + + const side1LightDir = getVec(new Vec3(...side1Dir), dirVec) + const side1Light = world.getLight(cursor.plus(side1LightDir)) / 15 + const side2DirLight = getVec(new Vec3(...side2Dir), dirVec) + const side2Light = world.getLight(cursor.plus(side2DirLight)) / 15 + const cornerLightDir = getVec(new Vec3(...cornerDir), dirVec) + const cornerLight = world.getLight(cursor.plus(cornerLightDir)) / 15 + // interpolate + const lights = [side1Light, side2Light, cornerLight, baseLight] + cornerLightResult = lights.reduce((acc, cur) => acc + cur, 0) / lights.length + } + + // Apply light value to tint + attr.t_colors.push(tint[0] * cornerLightResult, tint[1] * cornerLightResult, tint[2] * cornerLightResult) } } } @@ -476,7 +510,7 @@ export function getSectionGeometry (sx, sy, sz, world: World) { heads: {}, signs: {}, // isFull: true, - highestBlocks: {}, + highestBlocks: new Map(), hadErrors: false, blocksCount: 0 } @@ -487,9 +521,9 @@ export function getSectionGeometry (sx, sy, sz, world: World) { for (cursor.x = sx; cursor.x < sx + 16; cursor.x++) { let block = world.getBlock(cursor, blockProvider, attr)! if (!INVISIBLE_BLOCKS.has(block.name)) { - const highest = attr.highestBlocks[`${cursor.x},${cursor.z}`] + const highest = attr.highestBlocks.get(`${cursor.x},${cursor.z}`) if (!highest || highest.y < cursor.y) { - attr.highestBlocks[`${cursor.x},${cursor.z}`] = { y: cursor.y, stateId: block.stateId, biomeId: block.biome.id } + attr.highestBlocks.set(`${cursor.x},${cursor.z}`, { y: cursor.y, stateId: block.stateId, biomeId: block.biome.id }) } } if (INVISIBLE_BLOCKS.has(block.name)) continue diff --git a/renderer/viewer/lib/mesher/shared.ts b/renderer/viewer/lib/mesher/shared.ts index eb1346f4..82416fab 100644 --- a/renderer/viewer/lib/mesher/shared.ts +++ b/renderer/viewer/lib/mesher/shared.ts @@ -3,6 +3,8 @@ import { BlockType } from '../../../playground/shared' // only here for easier testing export const defaultMesherConfig = { version: '', + worldMaxY: 256, + worldMinY: 0, enableLighting: true, skyLight: 15, smoothLighting: true, @@ -40,12 +42,21 @@ export type MesherGeometryOutput = { heads: Record, signs: Record, // isFull: boolean - highestBlocks: Record + highestBlocks: Map hadErrors: boolean blocksCount: number customBlockModels?: CustomBlockModels } +export interface MesherMainEvents { + geometry: { type: 'geometry'; key: string; geometry: MesherGeometryOutput; workerIndex: number }; + sectionFinished: { type: 'sectionFinished'; key: string; workerIndex: number; processTime?: number }; + blockStateModelInfo: { type: 'blockStateModelInfo'; info: Record }; + heightmap: { type: 'heightmap'; key: string; heightmap: Uint8Array }; +} + +export type MesherMainEvent = MesherMainEvents[keyof MesherMainEvents] + export type HighestBlockInfo = { y: number, stateId: number | undefined, biomeId: number | undefined } export type BlockStateModelInfo = { diff --git a/renderer/viewer/lib/mesher/test/tests.test.ts b/renderer/viewer/lib/mesher/test/tests.test.ts index 7959f573..9ebd6604 100644 --- a/renderer/viewer/lib/mesher/test/tests.test.ts +++ b/renderer/viewer/lib/mesher/test/tests.test.ts @@ -49,7 +49,6 @@ test('Known blocks are not rendered', () => { // TODO resolve creaking_heart issue (1.21.3) expect(missingBlocks).toMatchInlineSnapshot(` { - "creaking_heart": true, "end_gateway": true, "end_portal": true, "structure_void": true, diff --git a/renderer/viewer/lib/worldrendererCommon.ts b/renderer/viewer/lib/worldrendererCommon.ts index 29332690..88f36551 100644 --- a/renderer/viewer/lib/worldrendererCommon.ts +++ b/renderer/viewer/lib/worldrendererCommon.ts @@ -15,7 +15,7 @@ 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 { HighestBlockInfo, MesherGeometryOutput, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig, MesherMainEvent } from './mesher/shared' import { chunkPos } from './simpleUtils' import { addNewStat, removeAllStats, removeStat, updatePanesVisibility, updateStatText } from './ui/newStats' import { WorldDataEmitter } from './worldDataEmitter' @@ -44,6 +44,7 @@ export const defaultWorldRendererConfig = { starfield: true, addChunksBatchWaitTime: 200, vrSupport: true, + vrPageGameRendering: true, renderEntities: true, fov: 75, fetchPlayerSkins: true, @@ -97,6 +98,7 @@ export abstract class WorldRendererCommon dirty (pos: Vec3, value: boolean): void update (/* pos: Vec3, value: boolean */): void chunkFinished (key: string): void + heightmap (key: string, heightmap: Uint8Array): void }> customTexturesDataUrl = undefined as string | undefined workers: any[] = [] @@ -115,8 +117,8 @@ export abstract class WorldRendererCommon ONMESSAGE_TIME_LIMIT = 30 // ms handleResize = () => { } - highestBlocksByChunks = {} as Record - highestBlocksBySections = {} as Record + highestBlocksByChunks = new Map() + highestBlocksBySections = new Map() blockEntities = {} workersProcessAverageTime = 0 @@ -259,7 +261,7 @@ export abstract class WorldRendererCommon } async getHighestBlocks (chunkKey: string) { - return this.highestBlocksByChunks[chunkKey] + return this.highestBlocksByChunks.get(chunkKey) } updateCustomBlock (chunkKey: string, blockPos: string, model: string) { @@ -382,19 +384,20 @@ export abstract class WorldRendererCommon this.isProcessingQueue = false } - handleMessage (data) { + handleMessage (rawData: any) { + const data = rawData as MesherMainEvent if (!this.active) return this.mesherLogReader?.workerMessageReceived(data.type, data) if (data.type !== 'geometry' || !this.debugStopGeometryUpdate) { const start = performance.now() - this.handleWorkerMessage(data) + this.handleWorkerMessage(data as WorkerReceive) this.workerCustomHandleTime += performance.now() - start } if (data.type === 'geometry') { this.logWorkerWork(() => `-> ${data.workerIndex} geometry ${data.key} ${JSON.stringify({ dataSize: JSON.stringify(data).length })}`) this.geometryReceiveCount[data.workerIndex] ??= 0 this.geometryReceiveCount[data.workerIndex]++ - const geometry = data.geometry as MesherGeometryOutput + const { geometry } = data this.highestBlocksBySections[data.key] = geometry.highestBlocks const chunkCoords = data.key.split(',').map(Number) this.lastChunkDistance = Math.max(...this.getDistance(new Vec3(chunkCoords[0], 0, chunkCoords[2]))) @@ -421,6 +424,7 @@ export abstract class WorldRendererCommon if (loaded) { // CHUNK FINISHED this.finishedChunks[chunkKey] = true + this.reactiveState.world.chunksLoaded.add(`${Math.floor(chunkCoords[0] / 16)},${Math.floor(chunkCoords[2] / 16)}`) this.renderUpdateEmitter.emit(`chunkFinished`, `${chunkCoords[0]},${chunkCoords[2]}`) this.checkAllFinished() // merge highest blocks by sections into highest blocks by chunks @@ -459,6 +463,10 @@ export abstract class WorldRendererCommon this.blockStateModelInfo.set(cacheKey, info) } } + + if (data.type === 'heightmap') { + appViewer.rendererState.world.heightmaps.set(data.key, new Uint8Array(data.heightmap)) + } } downloadMesherLog () { @@ -569,7 +577,9 @@ export abstract class WorldRendererCommon textureSize: this.resourcesManager.currentResources!.blocksAtlasParser.atlas.latest.width, debugModelVariant: undefined, clipWorldBelowY: this.worldRendererConfig.clipWorldBelowY, - disableSignsMapsSupport: !this.worldRendererConfig.extraBlockRenderers + disableSignsMapsSupport: !this.worldRendererConfig.extraBlockRenderers, + worldMinY: this.worldMinYRender, + worldMaxY: this.worldMinYRender + this.worldSizeParams.worldHeight, } } @@ -616,7 +626,7 @@ export abstract class WorldRendererCommon updateChunksStats () { const loadedChunks = Object.keys(this.finishedChunks) - this.displayOptions.nonReactiveState.world.chunksLoaded = loadedChunks + this.displayOptions.nonReactiveState.world.chunksLoaded = new Set(loadedChunks) this.displayOptions.nonReactiveState.world.chunksTotalNumber = this.chunksLength this.reactiveState.world.allChunksLoaded = this.allChunksFinished @@ -645,6 +655,11 @@ export abstract class WorldRendererCommon customBlockModels: customBlockModels || undefined }) } + this.workers[0].postMessage({ + type: 'getHeightmap', + x, + z, + }) this.logWorkerWork(() => `-> chunk ${JSON.stringify({ x, z, chunkLength: chunk.length, customBlockModelsLength: customBlockModels ? Object.keys(customBlockModels).length : 0 })}`) this.mesherLogReader?.chunkReceived(x, z, chunk.length) for (let y = this.worldMinYRender; y < this.worldSizeParams.worldHeight; y += 16) { @@ -681,9 +696,9 @@ export abstract class WorldRendererCommon for (let y = this.worldSizeParams.minY; y < this.worldSizeParams.worldHeight; y += 16) { this.setSectionDirty(new Vec3(x, y, z), false) delete this.finishedSections[`${x},${y},${z}`] - delete this.highestBlocksBySections[`${x},${y},${z}`] + this.highestBlocksBySections.delete(`${x},${y},${z}`) } - delete this.highestBlocksByChunks[`${x},${z}`] + this.highestBlocksByChunks.delete(`${x},${z}`) this.updateChunksStats() @@ -1009,7 +1024,6 @@ export abstract class WorldRendererCommon this.active = false this.renderUpdateEmitter.removeAllListeners() - this.displayOptions.worldView.removeAllListeners() // todo this.abortController.abort() removeAllStats() } diff --git a/renderer/viewer/three/cameraShake.ts b/renderer/viewer/three/cameraShake.ts index f6a61e2e..6fe483cc 100644 --- a/renderer/viewer/three/cameraShake.ts +++ b/renderer/viewer/three/cameraShake.ts @@ -1,4 +1,5 @@ import * as THREE from 'three' +import { WorldRendererThree } from './worldrendererThree' export class CameraShake { private rollAngle = 0 @@ -8,7 +9,7 @@ export class CameraShake { private basePitch = 0 private baseYaw = 0 - constructor (public camera: THREE.Camera, public onRenderCallbacks: Array<() => void>) { + constructor (public worldRenderer: WorldRendererThree, public onRenderCallbacks: Array<() => void>) { onRenderCallbacks.push(() => { this.update() }) @@ -62,14 +63,21 @@ export class CameraShake { } } - // Create rotation quaternions - const pitchQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), this.basePitch) - const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), this.baseYaw) - const rollQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), THREE.MathUtils.degToRad(this.rollAngle)) + const camera = this.worldRenderer.cameraGroupVr || this.worldRenderer.camera - // Combine rotations in the correct order: pitch -> yaw -> roll - const finalQuat = yawQuat.multiply(pitchQuat).multiply(rollQuat) - this.camera.setRotationFromQuaternion(finalQuat) + if (this.worldRenderer.cameraGroupVr) { + // For VR camera, only apply yaw rotation + const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), this.baseYaw) + camera.setRotationFromQuaternion(yawQuat) + } else { + // For regular camera, apply all rotations + const pitchQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), this.basePitch) + const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), this.baseYaw) + const rollQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), THREE.MathUtils.degToRad(this.rollAngle)) + // Combine rotations in the correct order: pitch -> yaw -> roll + const finalQuat = yawQuat.multiply(pitchQuat).multiply(rollQuat) + camera.setRotationFromQuaternion(finalQuat) + } } private easeOut (t: number): number { diff --git a/renderer/viewer/three/documentRenderer.ts b/renderer/viewer/three/documentRenderer.ts index 46672b74..1d556c2a 100644 --- a/renderer/viewer/three/documentRenderer.ts +++ b/renderer/viewer/three/documentRenderer.ts @@ -3,6 +3,7 @@ import Stats from 'stats.js' import StatsGl from 'stats-gl' import * as tween from '@tweenjs/tween.js' import { GraphicsBackendConfig, GraphicsInitOptions } from '../../../src/appViewer' +import { WorldRendererConfig } from '../lib/worldrendererCommon' export class DocumentRenderer { readonly canvas = document.createElement('canvas') @@ -23,6 +24,7 @@ export class DocumentRenderer { droppedFpsPercentage: number config: GraphicsBackendConfig onRender = [] as Array<(sizeChanged: boolean) => void> + inWorldRenderingConfig: WorldRendererConfig | undefined constructor (initOptions: GraphicsInitOptions) { this.config = initOptions.config @@ -94,7 +96,7 @@ export class DocumentRenderer { if (this.disconnected) return this.animationFrameId = requestAnimationFrame(animate) - if (this.paused) return + if (this.paused || (this.renderer.xr.isPresenting && !this.inWorldRenderingConfig?.vrPageGameRendering)) return // Handle FPS limiting if (this.config.fpsLimit) { @@ -117,18 +119,7 @@ export class DocumentRenderer { sizeChanged = true } - this.preRender() - this.stats.markStart() - tween.update() - if (!window.freezeRender) { - this.render(sizeChanged) - } - for (const fn of this.onRender) { - fn(sizeChanged) - } - this.renderedFps++ - this.stats.markEnd() - this.postRender() + this.frameRender(sizeChanged) // Update stats visibility each frame if (this.config.statsVisible !== undefined) { @@ -139,6 +130,21 @@ export class DocumentRenderer { animate() } + frameRender (sizeChanged: boolean) { + this.preRender() + this.stats.markStart() + tween.update() + if (!window.freezeRender) { + this.render(sizeChanged) + } + for (const fn of this.onRender) { + fn(sizeChanged) + } + this.renderedFps++ + this.stats.markEnd() + this.postRender() + } + setPaused (paused: boolean) { this.paused = paused } diff --git a/renderer/viewer/three/entities.ts b/renderer/viewer/three/entities.ts index 320daff9..1a5e7c3f 100644 --- a/renderer/viewer/three/entities.ts +++ b/renderer/viewer/three/entities.ts @@ -292,6 +292,9 @@ export class Entities { playerObject.animation.update(playerObject, dt) } + // Update armor positions + this.syncArmorPositions(entity) + // Update visibility based on distance and chunk load status if (botPos && entity.position) { const dx = entity.position.x - botPos.x @@ -312,6 +315,79 @@ export class Entities { } } + private syncArmorPositions (entity: SceneEntity) { + if (!entity.playerObject) return + + // todo-low use property access for less loop iterations (small performance gain) + entity.traverse((armor) => { + if (!armor.name.startsWith('geometry_armor_')) return + + const { skin } = entity.playerObject! + + switch (armor.name) { + case 'geometry_armor_head': + // Head armor sync + if (armor.children[0]?.children[0]) { + armor.children[0].children[0].rotation.set( + -skin.head.rotation.x, + skin.head.rotation.y, + skin.head.rotation.z, + skin.head.rotation.order + ) + } + break + + case 'geometry_armor_legs': + // Legs armor sync + if (armor.children[0]) { + // Left leg + if (armor.children[0].children[2]) { + armor.children[0].children[2].rotation.set( + -skin.leftLeg.rotation.x, + skin.leftLeg.rotation.y, + skin.leftLeg.rotation.z, + skin.leftLeg.rotation.order + ) + } + // Right leg + if (armor.children[0].children[1]) { + armor.children[0].children[1].rotation.set( + -skin.rightLeg.rotation.x, + skin.rightLeg.rotation.y, + skin.rightLeg.rotation.z, + skin.rightLeg.rotation.order + ) + } + } + break + + case 'geometry_armor_feet': + // Boots armor sync + if (armor.children[0]) { + // Right boot + if (armor.children[0].children[0]) { + armor.children[0].children[0].rotation.set( + -skin.rightLeg.rotation.x, + skin.rightLeg.rotation.y, + skin.rightLeg.rotation.z, + skin.rightLeg.rotation.order + ) + } + // Left boot (reversed Z rotation) + if (armor.children[0].children[1]) { + armor.children[0].children[1].rotation.set( + -skin.leftLeg.rotation.x, + skin.leftLeg.rotation.y, + -skin.leftLeg.rotation.z, + skin.leftLeg.rotation.order + ) + } + } + break + } + }) + } + getPlayerObject (entityId: string | number) { const playerObject = this.entities[entityId]?.playerObject return playerObject @@ -1203,6 +1279,16 @@ function addArmorModel (worldRenderer: WorldRendererThree, entityMesh: THREE.Obj }) } else { mesh = getMesh(worldRenderer, texturePath, armorModel[slotType]) + // // enable debug mode to see the mesh + // mesh.traverse(c => { + // if (c instanceof THREE.Mesh) { + // c.material.wireframe = true + // } + // }) + if (slotType === 'head') { + // avoid z-fighting with the head + mesh.children[0].position.y += 0.01 + } mesh.name = meshName material = mesh.material if (!isPlayerHead) { diff --git a/renderer/viewer/three/graphicsBackend.ts b/renderer/viewer/three/graphicsBackend.ts index 6ac068d7..37acfde9 100644 --- a/renderer/viewer/three/graphicsBackend.ts +++ b/renderer/viewer/three/graphicsBackend.ts @@ -8,6 +8,7 @@ import supportedVersions from '../../../src/supportedVersions.mjs' import { WorldRendererThree } from './worldrendererThree' import { DocumentRenderer } from './documentRenderer' import { PanoramaRenderer } from './panorama' +import { initVR } from './world/vr' // https://discourse.threejs.org/t/updates-to-color-management-in-three-js-r152/50791 THREE.ColorManagement.enabled = false @@ -87,10 +88,12 @@ const createGraphicsBackend: GraphicsBackendLoader = (initOptions: GraphicsInitO panoramaRenderer = null } worldRenderer = new WorldRendererThree(documentRenderer.renderer, initOptions, displayOptions) + void initVR(worldRenderer, documentRenderer) await worldRenderer.worldReadyPromise documentRenderer.render = (sizeChanged: boolean) => { worldRenderer?.render(sizeChanged) } + documentRenderer.inWorldRenderingConfig = displayOptions.inWorldRenderingConfig window.world = worldRenderer callModsMethod('worldReady', worldRenderer) } diff --git a/renderer/viewer/three/holdingBlock.ts b/renderer/viewer/three/holdingBlock.ts index 3f1327f1..6836d4f0 100644 --- a/renderer/viewer/three/holdingBlock.ts +++ b/renderer/viewer/three/holdingBlock.ts @@ -1,5 +1,6 @@ import * as THREE from 'three' import * as tweenJs from '@tweenjs/tween.js' +import PrismarineItem from 'prismarine-item' import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider' import { BlockModel } from 'mc-assets' import { getThreeBlockModelGroup, renderBlockThree, setBlockPosition } from '../lib/mesher/standaloneRenderer' @@ -293,6 +294,7 @@ export default class HoldingBlock { } isDifferentItem (block: HandItemBlock | undefined) { + const Item = PrismarineItem(this.worldRenderer.version) if (!this.lastHeldItem) { return true } @@ -300,7 +302,7 @@ export default class HoldingBlock { return true } // eslint-disable-next-line sonarjs/prefer-single-boolean-return - if (JSON.stringify(this.lastHeldItem.fullItem) !== JSON.stringify(block?.fullItem ?? '{}')) { + if (!Item.equal(this.lastHeldItem.fullItem, block?.fullItem ?? {}) || JSON.stringify(this.lastHeldItem.fullItem.components) !== JSON.stringify(block?.fullItem?.components)) { return true } diff --git a/renderer/viewer/three/world/vr.ts b/renderer/viewer/three/world/vr.ts index 925ba0bb..c2665585 100644 --- a/renderer/viewer/three/world/vr.ts +++ b/renderer/viewer/three/world/vr.ts @@ -4,8 +4,9 @@ import { XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerM import { buttonMap as standardButtonsMap } from 'contro-max/build/gamepad' import * as THREE from 'three' import { WorldRendererThree } from '../worldrendererThree' +import { DocumentRenderer } from '../documentRenderer' -export async function initVR (worldRenderer: WorldRendererThree) { +export async function initVR (worldRenderer: WorldRendererThree, documentRenderer: DocumentRenderer) { if (!('xr' in navigator) || !worldRenderer.worldRendererConfig.vrSupport) return const { renderer } = worldRenderer @@ -26,12 +27,13 @@ export async function initVR (worldRenderer: WorldRendererThree) { function enableVr () { renderer.xr.enabled = true + // renderer.xr.setReferenceSpaceType('local-floor') worldRenderer.reactiveState.preventEscapeMenu = true } function disableVr () { renderer.xr.enabled = false - worldRenderer.cameraObjectOverride = undefined + worldRenderer.cameraGroupVr = undefined worldRenderer.reactiveState.preventEscapeMenu = false worldRenderer.scene.remove(user) vrButtonContainer.hidden = true @@ -189,7 +191,7 @@ export async function initVR (worldRenderer: WorldRendererThree) { } // appViewer.backend?.updateCamera(null, yawOffset, 0) - worldRenderer.updateCamera(null, bot.entity.yaw, bot.entity.pitch) + // worldRenderer.updateCamera(null, bot.entity.yaw, bot.entity.pitch) // todo restore this logic (need to preserve ability to move camera) // const xrCamera = renderer.xr.getCamera() @@ -197,16 +199,13 @@ export async function initVR (worldRenderer: WorldRendererThree) { // bot.entity.yaw = Math.atan2(-d.x, -d.z) // bot.entity.pitch = Math.asin(d.y) - // todo ? - // bot.physics.stepHeight = 1 - - worldRenderer.render() + documentRenderer.frameRender(false) }) renderer.xr.addEventListener('sessionstart', () => { - worldRenderer.cameraObjectOverride = user + worldRenderer.cameraGroupVr = user }) renderer.xr.addEventListener('sessionend', () => { - worldRenderer.cameraObjectOverride = undefined + worldRenderer.cameraGroupVr = undefined }) worldRenderer.abortController.signal.addEventListener('abort', disableVr) diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index 8f1ddcca..0634d536 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -20,7 +20,6 @@ import { armorModel } from './entity/armorModels' import { disposeObject } from './threeJsUtils' import { CursorBlock } from './world/cursorBlock' import { getItemUv } from './appShared' -import { initVR } from './world/vr' import { Entities } from './entities' import { ThreeJsSound } from './threeJsSound' import { CameraShake } from './cameraShake' @@ -42,7 +41,7 @@ export class WorldRendererThree extends WorldRendererCommon { ambientLight = new THREE.AmbientLight(0xcc_cc_cc) directionalLight = new THREE.DirectionalLight(0xff_ff_ff, 0.5) entities = new Entities(this) - cameraObjectOverride?: THREE.Object3D // for xr + cameraGroupVr?: THREE.Object3D material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.1 }) itemsTexture: THREE.Texture cursorBlock = new CursorBlock(this) @@ -91,10 +90,9 @@ export class WorldRendererThree extends WorldRendererCommon { this.addDebugOverlay() this.resetScene() void this.init() - void initVR(this) this.soundSystem = new ThreeJsSound(this) - this.cameraShake = new CameraShake(this.camera, this.onRender) + this.cameraShake = new CameraShake(this, this.onRender) this.media = new ThreeJsMedia(this) // this.fountain = new Fountain(this.scene, this.scene, { // position: new THREE.Vector3(0, 10, 0), @@ -106,6 +104,10 @@ export class WorldRendererThree extends WorldRendererCommon { this.worldSwitchActions() } + get cameraObject () { + return this.cameraGroupVr || this.camera + } + worldSwitchActions () { this.onWorldSwitched.push(() => { // clear custom blocks @@ -301,7 +303,7 @@ export class WorldRendererThree extends WorldRendererCommon { updateViewerPosition (pos: Vec3): void { this.viewerPosition = pos - const cameraPos = this.camera.position.toArray().map(x => Math.floor(x / 16)) as [number, number, number] + const cameraPos = this.cameraObject.position.toArray().map(x => Math.floor(x / 16)) as [number, number, number] this.cameraSectionPos = new Vec3(...cameraPos) // eslint-disable-next-line guard-for-in for (const key in this.sectionObjects) { @@ -429,10 +431,8 @@ export class WorldRendererThree extends WorldRendererCommon { } setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number) { - const cam = this.cameraObjectOverride || this.camera const yOffset = this.displayOptions.playerState.getEyeHeight() - this.camera = cam as THREE.PerspectiveCamera this.updateCamera(pos?.offset(0, yOffset, 0) ?? null, yaw, pitch) this.media.tryIntersectMedia() } @@ -445,7 +445,11 @@ export class WorldRendererThree extends WorldRendererCommon { // } if (pos) { - new tweenJs.Tween(this.camera.position).to({ x: pos.x, y: pos.y, z: pos.z }, 50).start() + if (this.renderer.xr.isPresenting) { + pos.y -= this.camera.position.y // Fix Y position of camera in world + } + + new tweenJs.Tween(this.cameraObject.position).to({ x: pos.x, y: pos.y, z: pos.z }, 50).start() // this.freeFlyState.position = pos } this.cameraShake.setBaseRotation(pitch, yaw) @@ -495,10 +499,10 @@ export class WorldRendererThree extends WorldRendererCommon { } // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style - const cam = this.camera instanceof THREE.Group ? this.camera.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera + const cam = this.cameraGroupVr instanceof THREE.Group ? this.cameraGroupVr.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera this.renderer.render(this.scene, cam) - if (this.displayOptions.inWorldRenderingConfig.showHand/* && !this.freeFlyMode */) { + if (this.displayOptions.inWorldRenderingConfig.showHand && !this.playerState.shouldHideHand /* && !this.freeFlyMode */ && !this.renderer.xr.isPresenting) { this.holdingBlock.render(this.camera, this.renderer, this.ambientLight, this.directionalLight) this.holdingBlockLeft.render(this.camera, this.renderer, this.ambientLight, this.directionalLight) } diff --git a/server.js b/server.js index 20e66051..49699cdb 100644 --- a/server.js +++ b/server.js @@ -16,9 +16,23 @@ try { const app = express() const isProd = process.argv.includes('--prod') || process.env.NODE_ENV === 'production' +const timeoutIndex = process.argv.indexOf('--timeout') +let timeout = timeoutIndex > -1 && timeoutIndex + 1 < process.argv.length + ? parseInt(process.argv[timeoutIndex + 1]) + : process.env.TIMEOUT + ? parseInt(process.env.TIMEOUT) + : 10000 +if (isNaN(timeout) || timeout < 0) { + console.warn('Invalid timeout value provided, using default of 10000ms') + timeout = 10000 +} app.use(compression()) app.use(cors()) -app.use(netApi({ allowOrigin: '*' })) +app.use(netApi({ + allowOrigin: '*', + log: process.argv.includes('--log') || process.env.LOG === 'true', + timeout +})) if (!isProd) { app.use('/sounds', express.static(path.join(__dirname, './generated/sounds/'))) } diff --git a/src/appConfig.ts b/src/appConfig.ts index 156c5974..48b3665a 100644 --- a/src/appConfig.ts +++ b/src/appConfig.ts @@ -5,6 +5,27 @@ import { setLoadingScreenStatus } from './appStatus' import { setStorageDataOnAppConfigLoad } from './react/appStorageProvider' import { customKeymaps, updateBinds } from './controls' +export type CustomAction = { + readonly type: string + readonly input: readonly any[] +} + +export type ActionType = string | CustomAction + +export type ActionHoldConfig = { + readonly command: ActionType + readonly longPressAction?: ActionType + readonly duration?: number + readonly threshold?: number +} + +export type MobileButtonConfig = { + readonly label?: string + readonly icon?: string + readonly action?: ActionType + readonly actionHold?: ActionType | ActionHoldConfig +} + export type AppConfig = { // defaultHost?: string // defaultHostSave?: string @@ -24,7 +45,9 @@ export type AppConfig = { // hideSettings?: Record allowAutoConnect?: boolean splashText?: string + splashTextFallback?: string pauseLinks?: Array>> + mobileButtons?: MobileButtonConfig[] keybindings?: Record defaultLanguage?: string displayLanguageSelector?: boolean diff --git a/src/appViewer.ts b/src/appViewer.ts index ca62bd1b..f4a21481 100644 --- a/src/appViewer.ts +++ b/src/appViewer.ts @@ -17,7 +17,8 @@ import { watchOptionsAfterWorldViewInit } from './watchOptions' export interface RendererReactiveState { world: { - chunksLoaded: string[] + chunksLoaded: Set + heightmaps: Map chunksTotalNumber: number allChunksLoaded: boolean mesherWork: boolean @@ -28,7 +29,7 @@ export interface RendererReactiveState { } export interface NonReactiveState { world: { - chunksLoaded: string[] + chunksLoaded: Set chunksTotalNumber: number allChunksLoaded: boolean mesherWork: boolean @@ -198,7 +199,7 @@ export class AppViewer { resetBackend (cleanState = false) { this.disconnectBackend(cleanState) if (this.backendLoader) { - this.loadBackend(this.backendLoader) + void this.loadBackend(this.backendLoader) } } diff --git a/src/cameraRotationControls.ts b/src/cameraRotationControls.ts index 3368666f..849e5940 100644 --- a/src/cameraRotationControls.ts +++ b/src/cameraRotationControls.ts @@ -74,8 +74,6 @@ export const onControInit = () => { } function pointerLockChangeCallback () { - hideNotification('pointerlockchange') - if (appViewer.rendererState.preventEscapeMenu) return if (!pointerLock.hasPointerLock && activeModalStack.length === 0 && miscUiState.gameLoaded) { showModal({ reactType: 'pause-screen' }) diff --git a/src/connect.ts b/src/connect.ts index b68e4325..914303c6 100644 --- a/src/connect.ts +++ b/src/connect.ts @@ -21,7 +21,6 @@ export type ConnectOptions = { peerId?: string ignoreQs?: boolean onSuccessfulPlay?: () => void - autoLoginPassword?: string serverIndex?: string authenticatedAccount?: AuthenticatedAccount | true peerOptions?: any diff --git a/src/controls.ts b/src/controls.ts index 730bbbf5..f32cbda6 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -27,7 +27,8 @@ import { onCameraMove, onControInit } from './cameraRotationControls' import { createNotificationProgressReporter } from './core/progressReporter' import { appStorage } from './react/appStorageProvider' import { switchGameMode } from './packetsReplay/replayPackets' - +import { tabListState } from './react/PlayerListOverlayProvider' +import { type ActionType, type ActionHoldConfig, type CustomAction } from './appConfig' export const customKeymaps = proxy(appStorage.keybindings) subscribe(customKeymaps, () => { @@ -45,6 +46,7 @@ export const contro = new ControMax({ jump: ['Space', 'A'], inventory: ['KeyE', 'X'], drop: ['KeyQ', 'B'], + dropStack: [null], sneak: ['ShiftLeft'], toggleSneakOrDown: [null, 'Right Stick'], sprint: ['ControlLeft', 'Left Stick'], @@ -54,7 +56,7 @@ export const contro = new ControMax({ attackDestroy: [null, 'Right Trigger'], interactPlace: [null, 'Left Trigger'], swapHands: ['KeyF'], - selectItem: ['KeyH'], // default will be removed + selectItem: ['KeyH'], rotateCameraLeft: [null], rotateCameraRight: [null], rotateCameraUp: [null], @@ -62,6 +64,9 @@ export const contro = new ControMax({ // ui? chat: [['KeyT', 'Enter']], command: ['Slash'], + playersList: ['Tab'], + debugOverlay: ['F3'], + debugOverlayHelpMenu: [null], // client side zoom: ['KeyC'], viewerConsole: ['Backquote'], @@ -76,7 +81,7 @@ export const contro = new ControMax({ pauseMenu: [null, 'Start'] }, communication: { - toggleMicrophone: ['KeyK'], + toggleMicrophone: ['KeyM'], }, advanced: { lockUrl: ['KeyY'], @@ -235,6 +240,10 @@ const inModalCommand = (command: Command, pressed: boolean) => { if (command === 'ui.back') { hideCurrentModal() } + if (command === 'ui.pauseMenu') { + // hide all modals + hideAllModals() + } if (command === 'ui.leftClick' || command === 'ui.rightClick') { // in percent const { x, y } = gamepadUiCursorState @@ -399,12 +408,45 @@ const onTriggerOrReleased = (command: Command, pressed: boolean) => { case 'general.zoom': gameAdditionalState.isZooming = pressed break + case 'general.debugOverlay': + if (pressed) { + miscUiState.showDebugHud = !miscUiState.showDebugHud + } + break + case 'general.debugOverlayHelpMenu': + if (pressed) { + void onF3LongPress() + } + break case 'general.rotateCameraLeft': case 'general.rotateCameraRight': case 'general.rotateCameraUp': case 'general.rotateCameraDown': cameraRotationControls.handleCommand(command, pressed) break + case 'general.playersList': + tabListState.isOpen = pressed + break + } + } else if (stringStartsWith(command, 'ui')) { + switch (command) { + case 'ui.pauseMenu': + if (pressed) { + if (activeModalStack.length) { + hideCurrentModal() + } else { + showModal({ reactType: 'pause-screen' }) + } + } + break + case 'ui.back': + case 'ui.toggleFullscreen': + case 'ui.toggleMap': + case 'ui.leftClick': + case 'ui.rightClick': + case 'ui.speedupCursor': + // These are handled elsewhere + break } } } @@ -421,6 +463,9 @@ const alwaysPressedHandledCommand = (command: Command) => { if (command === 'advanced.lockUrl') { lockUrl() } + if (command === 'communication.toggleMicrophone') { + toggleMicrophoneMuted?.() + } } export function lockUrl () { @@ -494,6 +539,9 @@ contro.on('trigger', ({ command }) => { case 'general.rotateCameraRight': case 'general.rotateCameraUp': case 'general.rotateCameraDown': + case 'general.debugOverlay': + case 'general.debugOverlayHelpMenu': + case 'general.playersList': // no-op break case 'general.swapHands': { @@ -531,6 +579,12 @@ contro.on('trigger', ({ command }) => { } break } + case 'general.dropStack': { + if (bot.heldItem) { + void bot.tossStack(bot.heldItem) + } + break + } case 'general.chat': showModal({ reactType: 'chat' }) break @@ -557,14 +611,6 @@ contro.on('trigger', ({ command }) => { } } - if (command === 'communication.toggleMicrophone') { - // toggleMicrophoneMuted() - } - - if (command === 'ui.pauseMenu') { - showModal({ reactType: 'pause-screen' }) - } - if (command === 'ui.toggleFullscreen') { void goFullscreen(true) } @@ -714,30 +760,18 @@ export const f3Keybinds: Array<{ } ] -const hardcodedPressedKeys = new Set() document.addEventListener('keydown', (e) => { if (!isGameActive(false)) return - if (hardcodedPressedKeys.has('F3')) { + if (contro.pressedKeys.has('F3')) { const keybind = f3Keybinds.find((v) => v.key === e.code) if (keybind && (keybind.enabled?.() ?? true)) { void keybind.action() e.stopPropagation() } - return } - - hardcodedPressedKeys.add(e.code) }, { capture: true, }) -document.addEventListener('keyup', (e) => { - hardcodedPressedKeys.delete(e.code) -}) -document.addEventListener('visibilitychange', (e) => { - if (document.visibilityState === 'hidden') { - hardcodedPressedKeys.clear() - } -}) const isFlying = () => (bot.entity as any).flying @@ -893,3 +927,52 @@ export function updateBinds (commands: any) { })) } } + +export const onF3LongPress = async () => { + const select = await showOptionsModal('', f3Keybinds.filter(f3Keybind => { + return f3Keybind.mobileTitle && (f3Keybind.enabled?.() ?? true) + }).map(f3Keybind => { + return `${f3Keybind.mobileTitle}${f3Keybind.key ? ` (F3+${f3Keybind.key})` : ''}` + })) + if (!select) return + const f3Keybind = f3Keybinds.find(f3Keybind => f3Keybind.mobileTitle === select) + if (f3Keybind) void f3Keybind.action() +} + +export const handleMobileButtonCustomAction = (action: CustomAction) => { + const handler = customCommandsConfig[action.type]?.handler + if (handler) { + handler([...action.input]) + } +} + +export const handleMobileButtonActionCommand = (command: ActionType | ActionHoldConfig, isDown: boolean) => { + const commandValue = typeof command === 'string' ? command : 'command' in command ? command.command : command + + if (typeof commandValue === 'string' && !stringStartsWith(commandValue, 'custom')) { + const event: CommandEventArgument = { + command: commandValue as Command, + schema: { + keys: [], + gamepad: [] + } + } + if (isDown) { + contro.emit('trigger', event) + } else { + contro.emit('release', event) + } + } else if (typeof commandValue === 'object') { + if (isDown) { + handleMobileButtonCustomAction(commandValue) + } + } +} + +export const handleMobileButtonLongPress = (actionHold: ActionHoldConfig) => { + if (typeof actionHold.longPressAction === 'string' && actionHold.longPressAction === 'general.debugOverlayHelpMenu') { + void onF3LongPress() + } else if (actionHold.longPressAction) { + handleMobileButtonActionCommand(actionHold.longPressAction, true) + } +} diff --git a/src/entities.ts b/src/entities.ts index 919d6818..8ee36431 100644 --- a/src/entities.ts +++ b/src/entities.ts @@ -13,7 +13,7 @@ const updateAutoJump = () => { if (!bot?.autoJumper) return const autoJump = options.autoParkour || (options.autoJump === 'auto' ? miscUiState.currentTouch && !miscUiState.usingGamepadInput : options.autoJump === 'always') bot.autoJumper.setOpts({ - jumpIntoWater: options.autoParkour, + // jumpIntoWater: options.autoParkour, jumpOnAllEdges: options.autoParkour, // strictBlockCollision: true, }) @@ -61,7 +61,7 @@ customEvents.on('gameLoaded', () => { if (!tracking) continue const e = bot.entities[id] if (!e) continue - const speed = info.avgSpeed + const speed = info.avgVel const WALKING_SPEED = 0.03 const SPRINTING_SPEED = 0.18 const isCrouched = e['crouching'] diff --git a/src/flyingSquidUtils.ts b/src/flyingSquidUtils.ts index 012830d9..2ae0be7c 100644 --- a/src/flyingSquidUtils.ts +++ b/src/flyingSquidUtils.ts @@ -18,9 +18,10 @@ export function nameToMcOfflineUUID (name) { } export async function savePlayers (autoSave: boolean) { + if (!localServer?.players[0]) return if (autoSave && new URL(location.href).searchParams.get('noSave') === 'true') return //@ts-expect-error TODO - await localServer!.savePlayersSingleplayer() + await localServer.savePlayersSingleplayer() } // todo flying squid should expose save function instead diff --git a/src/globalDomListeners.ts b/src/globalDomListeners.ts index 5055c600..bfce0d42 100644 --- a/src/globalDomListeners.ts +++ b/src/globalDomListeners.ts @@ -35,3 +35,12 @@ window.addEventListener('beforeunload', (event) => { event.returnValue = '' // Required for some browsers return 'The game is running. Are you sure you want to close this page?' }) + +window.addEventListener('contextmenu', (e) => { + const ALLOW_TAGS = ['INPUT', 'TEXTAREA', 'A'] + // allow if target is in ALLOW_TAGS or has selection text + if (ALLOW_TAGS.includes((e.target as HTMLElement)?.tagName) || window.getSelection()?.toString()) { + return + } + e.preventDefault() +}) diff --git a/src/globalState.ts b/src/globalState.ts index bd845195..671d7907 100644 --- a/src/globalState.ts +++ b/src/globalState.ts @@ -121,6 +121,7 @@ export const miscUiState = proxy({ /** wether game hud is shown (in playing state) */ gameLoaded: false, showUI: true, + showDebugHud: false, loadedServerIndex: '', /** currently trying to load or loaded mc version, after all data is loaded */ loadedDataVersion: null as string | null, diff --git a/src/globals.d.ts b/src/globals.d.ts index b8741a12..7a2c6f1f 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -27,8 +27,14 @@ declare const customEvents: import('typed-emitter').default<{ search (q: string): void activateItem (item: Item, slot: number, offhand: boolean): void hurtAnimation (yaw?: number): void + customChannelRegister (channel: string, parser: any): void }> declare const beforeRenderFrame: Array<() => void> +declare const translate: (key: T) => T + +// API LAYER +declare const toggleMicrophoneMuted: undefined | (() => void) +declare const translateText: undefined | ((text: string) => string) declare interface Document { exitPointerLock?(): void diff --git a/src/globals.js b/src/globals.js index 1aa141c6..f9125c8c 100644 --- a/src/globals.js +++ b/src/globals.js @@ -9,3 +9,7 @@ window.viewer = undefined window.loadedData = undefined window.customEvents = new EventEmitter() window.customEvents.setMaxListeners(10_000) +window.translate = (key) => { + if (typeof key !== 'string') return key + return window.translateText?.(key) ?? key +} diff --git a/src/index.ts b/src/index.ts index 26bba494..bea10726 100644 --- a/src/index.ts +++ b/src/index.ts @@ -77,7 +77,7 @@ import './water' import { ConnectOptions, getVersionAutoSelect, downloadOtherGameData, downloadAllMinecraftData } from './connect' import { ref, subscribe } from 'valtio' import { signInMessageState } from './react/SignInMessageProvider' -import { updateAuthenticatedAccountData, updateLoadedServerData, updateServerConnectionHistory } from './react/serversStorage' +import { findServerPassword, updateAuthenticatedAccountData, updateLoadedServerData, updateServerConnectionHistory } from './react/serversStorage' import { mainMenuState } from './react/MainMenuRenderApp' import './mobileShim' import { parseFormattedMessagePacket } from './botUtils' @@ -708,7 +708,7 @@ export async function connect (connectOptions: ConnectOptions) { resolve() unsub() } else { - const perc = Math.round(appViewer.rendererState.world.chunksLoaded.length / appViewer.rendererState.world.chunksTotalNumber * 100) + const perc = Math.round(appViewer.rendererState.world.chunksLoaded.size / appViewer.rendererState.world.chunksTotalNumber * 100) progress?.reportProgress('chunks', perc / 100) } }) @@ -754,9 +754,10 @@ export async function connect (connectOptions: ConnectOptions) { } connectOptions.onSuccessfulPlay?.() updateDataAfterJoin() - if (connectOptions.autoLoginPassword) { + const password = findServerPassword() + if (password) { setTimeout(() => { - bot.chat(`/login ${connectOptions.autoLoginPassword}`) + bot.chat(`/login ${password}`) }, 500) } diff --git a/src/interactionShapesGenerated.json b/src/interactionShapesGenerated.json index afd3ce0f..804952e0 100644 --- a/src/interactionShapesGenerated.json +++ b/src/interactionShapesGenerated.json @@ -1318,55 +1318,47 @@ 13 ], "lever": { - "face=ceiling,facing=east": [ - 4, - 0, - 5, - 12, - 6, - 11 - ], - "face=ceiling,facing=north": [ - 5, - 0, - 4, - 11, - 6, - 12 - ], - "face=ceiling,facing=south": [ - 5, - 0, - 4, - 11, - 6, - 12 - ], - "face=ceiling,facing=west": [ - 4, - 0, - 5, - 12, - 6, - 11 - ], "face=floor,facing=east": [ 4, - 10, + 0, 5, 12, - 16, + 6, 11 ], "face=floor,facing=north": [ 5, - 10, + 0, 4, 11, - 16, + 6, 12 ], "face=floor,facing=south": [ + 5, + 0, + 4, + 11, + 6, + 12 + ], + "face=floor,facing=west": [ + 4, + 0, + 5, + 12, + 6, + 11 + ], + "face=ceiling,facing=east": [ + 4, + 10, + 5, + 12, + 16, + 11 + ], + "face=ceiling,facing=north": [ 5, 10, 4, @@ -1374,7 +1366,15 @@ 16, 12 ], - "face=floor,facing=west": [ + "face=ceiling,facing=south": [ + 5, + 10, + 4, + 11, + 16, + 12 + ], + "face=ceiling,facing=west": [ 4, 10, 5, diff --git a/src/inventoryWindows.ts b/src/inventoryWindows.ts index 2906177b..187bb997 100644 --- a/src/inventoryWindows.ts +++ b/src/inventoryWindows.ts @@ -25,6 +25,7 @@ import { GeneralInputItem, getItemMetadata, getItemModelName, getItemNameRaw, Re const loadedImagesCache = new Map() const cleanLoadedImagesCache = () => { loadedImagesCache.delete('blocks') + loadedImagesCache.delete('items') } let lastWindow: ReturnType @@ -120,6 +121,7 @@ export const onGameLoad = () => { if (!appViewer.resourcesManager['_inventoryChangeTracked']) { appViewer.resourcesManager['_inventoryChangeTracked'] = true const texturesChanged = () => { + cleanLoadedImagesCache() if (!lastWindow) return upWindowItemsLocal() upJei(lastJeiSearch) @@ -189,7 +191,6 @@ export const renderSlot = (model: ResolvedItemModelRender, debugIsQuickbar = fal image?: HTMLImageElement } | undefined => { let itemModelName = model.modelName - const originalItemName = itemModelName const isItem = loadedData.itemsByName[itemModelName] // #region normalize item name @@ -225,7 +226,7 @@ export const renderSlot = (model: ResolvedItemModelRender, debugIsQuickbar = fal ?? (model.originalItemName ? appViewer.resourcesManager.currentResources.itemsRenderer.getItemTexture(model.originalItemName, {}, false, fullBlockModelSupport) : undefined) ?? appViewer.resourcesManager.currentResources.itemsRenderer.getItemTexture('item/missing_texture')! } catch (err) { - inGameError(`Failed to render item ${itemModelName} (original: ${originalItemName}) on ${bot.version} (resourcepack: ${options.enabledResourcepack}): ${err.stack}`) + inGameError(`Failed to render item ${itemModelName} (original: ${model.originalItemName}) on ${bot.version} (resourcepack: ${options.enabledResourcepack}): ${err.stack}`) itemTexture = blockToTopTexture(appViewer.resourcesManager.currentResources!.itemsRenderer.getItemTexture('errored')!) } @@ -339,6 +340,7 @@ const implementedContainersGuiMap = { 'minecraft:generic_3x3': 'DropDispenseWin', 'minecraft:furnace': 'FurnaceWin', 'minecraft:smoker': 'FurnaceWin', + 'minecraft:blast_furnace': 'FurnaceWin', 'minecraft:crafting': 'CraftingWin', 'minecraft:crafting3x3': 'CraftingWin', // todo different result slot 'minecraft:anvil': 'AnvilWin', diff --git a/src/mineflayer/items.ts b/src/mineflayer/items.ts index 45638cd4..8a4e1fcd 100644 --- a/src/mineflayer/items.ts +++ b/src/mineflayer/items.ts @@ -92,7 +92,8 @@ export const getItemMetadata = (item: GeneralInputItem, resourcesManager: Resour export const getItemNameRaw = (item: Pick | null, resourcesManager: ResourcesManager) => { - const { customText } = getItemMetadata(item as any, resourcesManager) + if (!item) return '' + const { customText } = getItemMetadata(item as GeneralInputItem, resourcesManager) if (!customText) return try { if (typeof customText === 'object') { diff --git a/src/mineflayer/minecraft-protocol-extra.ts b/src/mineflayer/minecraft-protocol-extra.ts index e8216a00..f3cf11e3 100644 --- a/src/mineflayer/minecraft-protocol-extra.ts +++ b/src/mineflayer/minecraft-protocol-extra.ts @@ -3,13 +3,11 @@ import clientAutoVersion from 'minecraft-protocol/src/client/autoVersion' export const pingServerVersion = async (ip: string, port?: number, mergeOptions: Record = {}) => { const fakeClient = new EventEmitter() as any - fakeClient.on('error', (err) => { - throw new Error(err.message ?? err) - }) const options = { host: ip, port, - noPongTimeout: Infinity, // disable timeout + noPongTimeout: 10_000, + closeTimeout: 20_000, ...mergeOptions, } let latency = 0 @@ -19,12 +17,26 @@ export const pingServerVersion = async (ip: string, port?: number, mergeOptions: fullInfo = res }] - // TODO! use client.socket.destroy() instead of client.end() for faster cleanup - await clientAutoVersion(fakeClient, options) + // TODO use client.socket.destroy() instead of client.end() for faster cleanup + clientAutoVersion(fakeClient, options) + await Promise.race([ + new Promise((resolve, reject) => { + fakeClient.once('connect_allowed', () => { + resolve() + }) + }), + new Promise((resolve, reject) => { + fakeClient.on('error', (err) => { + reject(new Error(err.message ?? err)) + }) + if (mergeOptions.stream) { + mergeOptions.stream.on('end', (err) => { + reject(new Error('Connection closed')) + }) + } + }) + ]) - await new Promise((resolve, reject) => { - fakeClient.once('connect_allowed', resolve) - }) return { version: fakeClient.version, latency, diff --git a/src/mineflayer/playerState.ts b/src/mineflayer/playerState.ts index 85f0b00c..16739c86 100644 --- a/src/mineflayer/playerState.ts +++ b/src/mineflayer/playerState.ts @@ -24,6 +24,7 @@ export class PlayerStateManager implements IPlayerState { private itemUsageTicks = 0 private isUsingItem = false private ready = false + public lightingDisabled = false onlineMode = false get username () { return bot.username ?? '' @@ -51,6 +52,21 @@ export class PlayerStateManager implements IPlayerState { } private botCreated () { + const handleDimensionData = (data) => { + let hasSkyLight = 1 + try { + hasSkyLight = data.dimension.value.has_skylight.value + } catch {} + this.lightingDisabled = bot.game.dimension === 'the_nether' || bot.game.dimension === 'the_end' || !hasSkyLight + } + + bot._client.on('login', (packet) => { + handleDimensionData(packet) + }) + bot._client.on('respawn', (packet) => { + handleDimensionData(packet) + }) + // Movement tracking bot.on('move', this.updateState) @@ -75,6 +91,10 @@ export class PlayerStateManager implements IPlayerState { this.reactive.gameMode = bot.game?.gameMode } + get shouldHideHand () { + return this.reactive.gameMode === 'spectator' + } + // #region Movement and Physics State private updateState () { if (!bot?.entity || this.disableStateUpdates) return @@ -118,7 +138,7 @@ export class PlayerStateManager implements IPlayerState { } getEyeHeight (): number { - return bot.controlState.sneak ? 1.27 : 1.62 + return bot.controlState.sneak && !this.isFlying() ? 1.27 : 1.62 } isOnGround (): boolean { diff --git a/src/mineflayer/plugins/mouse.ts b/src/mineflayer/plugins/mouse.ts index 4e82b770..fc1ce0fd 100644 --- a/src/mineflayer/plugins/mouse.ts +++ b/src/mineflayer/plugins/mouse.ts @@ -10,7 +10,7 @@ import { sendVideoInteraction, videoCursorInteraction } from '../../customChanne function cursorBlockDisplay (bot: Bot) { const updateCursorBlock = (data?: { block: Block }) => { - if (!data?.block) { + if (!data?.block || bot.game.gameMode === 'spectator') { playerState.reactive.lookingAtBlock = undefined return } @@ -27,6 +27,10 @@ function cursorBlockDisplay (bot: Bot) { } bot.on('highlightCursorBlock', updateCursorBlock) + bot.on('game', () => { + const block = bot.mouse.getCursorState().cursorBlock + updateCursorBlock(block ? { block } : undefined) + }) bot.on('blockBreakProgressStage', (block, stage) => { const mergedShape = bot.mouse.getMergedCursorShape(block) diff --git a/src/mineflayer/websocket-core.ts b/src/mineflayer/websocket-core.ts index 482f0def..d24bd6be 100644 --- a/src/mineflayer/websocket-core.ts +++ b/src/mineflayer/websocket-core.ts @@ -33,10 +33,14 @@ export const getWebsocketStream = async (host: string) => { ws.addEventListener('close', () => { console.log('ws closed') clientDuplex.end() + setTimeout(() => { + clientDuplex.emit('end', 'Connection lost') + }, 500) }) ws.addEventListener('error', err => { console.log('ws error', err) + clientDuplex.emit('error', err) }) await new Promise((resolve, reject) => { diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx index b9c44b29..7f4c34ab 100644 --- a/src/optionsGuiScheme.tsx +++ b/src/optionsGuiScheme.tsx @@ -90,8 +90,7 @@ export const guiOptionsScheme: { }, lowMemoryMode: { text: 'Low Memory Mode', - enableWarning: 'Enabling it will make chunks load ~4x slower', - disabledDuringGame: true + enableWarning: 'Enabling it will make chunks load ~4x slower. When in the game, app needs to be reloaded to apply this setting.', }, starfieldRendering: {}, renderEntities: {}, @@ -286,6 +285,20 @@ export const guiOptionsScheme: { chatSelect: { }, }, + { + custom () { + return Map + }, + showMinimap: { + text: 'Enable Minimap', + enableWarning: 'App reload is required to apply this setting', + values: [ + 'always', + 'singleplayer', + 'never' + ], + }, + }, { custom () { return World @@ -321,19 +334,6 @@ export const guiOptionsScheme: { ], }, }, - { - custom () { - return Map - }, - showMinimap: { - text: 'Enable Minimap', - values: [ - 'always', - 'singleplayer', - 'never' - ], - }, - }, { custom () { return Experimental @@ -488,7 +488,11 @@ export const guiOptionsScheme: { ) }, - vrSupport: {} + vrSupport: {}, + vrPageGameRendering: { + text: 'Page Game Rendering', + tooltip: 'Wether to continue rendering page even when vr is active.', + } }, ], advanced: [ @@ -555,6 +559,7 @@ export const guiOptionsScheme: { { preventBackgroundTimeoutKick: {}, preventSleep: { + text: 'Prevent Device Sleep', disabledReason: navigator.wakeLock ? undefined : 'Your browser does not support wake lock API', enableWarning: 'When connected to a server, prevent PC from sleeping or screen dimming. Useful for purpusely staying AFK for long time. Some events might still prevent this like loosing tab focus or going low power mode.', }, @@ -605,6 +610,10 @@ export const guiOptionsScheme: { debugResponseTimeIndicator: { text: 'Debug Input Lag', }, + }, + { + debugChatScroll: { + }, } ], 'export-import': [ diff --git a/src/optionsStorage.ts b/src/optionsStorage.ts index 65c0b4ba..a0c995a0 100644 --- a/src/optionsStorage.ts +++ b/src/optionsStorage.ts @@ -72,6 +72,7 @@ const defaultOptions = { preventBackgroundTimeoutKick: false, preventSleep: false, debugContro: false, + debugChatScroll: false, chatVanillaRestrictions: true, debugResponseTimeIndicator: false, // antiAliasing: false, @@ -103,7 +104,8 @@ const defaultOptions = { autoJump: 'auto' as 'auto' | 'always' | 'never', autoParkour: false, vrSupport: true, // doesn't directly affect the VR mode, should only disable the button which is annoying to android users - renderDebug: (isDev ? 'advanced' : 'basic') as 'none' | 'advanced' | 'basic', + vrPageGameRendering: false, + renderDebug: 'basic' as 'none' | 'advanced' | 'basic', // advanced bot options autoRespawn: false, diff --git a/src/react/AddServerOrConnect.tsx b/src/react/AddServerOrConnect.tsx index 08ef7f29..08e4d69e 100644 --- a/src/react/AddServerOrConnect.tsx +++ b/src/react/AddServerOrConnect.tsx @@ -46,8 +46,7 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ const parsedInitialIp = parseServerAddress(initialData?.ip) const [serverName, setServerName] = React.useState(initialData?.name ?? qsParamName ?? '') - const [serverIp, setServerIp] = React.useState(parsedQsIp.host || parsedInitialIp.host || '') - const [serverPort, setServerPort] = React.useState(parsedQsIp.port || parsedInitialIp.port || '') + const [serverIp, setServerIp] = React.useState(parsedQsIp.serverIpFull || parsedInitialIp.serverIpFull || '') const [versionOverride, setVersionOverride] = React.useState(initialData?.versionOverride ?? /* legacy */ initialData?.['version'] ?? qsParamVersion ?? '') const [proxyOverride, setProxyOverride] = React.useState(initialData?.proxyOverride ?? qsParamProxy ?? '') const [usernameOverride, setUsernameOverride] = React.useState(initialData?.usernameOverride ?? qsParamUsername ?? '') @@ -61,7 +60,7 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ const noAccountSelected = accountIndex === -1 const authenticatedAccountOverride = noAccountSelected ? undefined : freshAccount ? true : accounts?.[accountIndex] - let ipFinal = serverIp.includes(':') ? serverIp : `${serverIp}${serverPort ? `:${serverPort}` : ''}` + let ipFinal = serverIp ipFinal = ipFinal.replace(/:$/, '') const commonUseOptions: BaseServerInfo = { name: serverName, @@ -126,6 +125,9 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ }, []) const displayConnectButton = qsParamIp + const serverExamples = ['example.com:25565', 'play.hypixel.net', 'ws://play.pcm.gg'] + // pick random example + const example = serverExamples[Math.floor(Math.random() * serverExamples.length)] return
- {!lockConnect && <> -
- setServerName(value)} placeholder='Defaults to IP' /> -
- } - setServerPort(value)} placeholder={serverIp.startsWith('ws://') || serverIp.startsWith('wss://') ? '' : '25565'} /> + {!lockConnect && <> +
+ setServerName(value)} placeholder='Defaults to IP' /> +
+ } {isSmallHeight ?
:
Overrides:
}
{ const sendHistoryRef = useRef(JSON.parse(window.sessionStorage.chatHistory || '[]')) const [isInputFocused, setIsInputFocused] = useState(false) @@ -86,7 +88,16 @@ export default ({ const chatHistoryPos = useRef(sendHistoryRef.current.length) const inputCurrentlyEnteredValue = useRef('') - const { scrollToBottom } = useScrollBehavior(chatMessages, { messages, opened }) + const { scrollToBottom, isAtBottom, wasAtBottom, currentlyAtBottom } = useScrollBehavior(chatMessages, { messages, opened }) + const [rightNowAtBottom, setRightNowAtBottom] = useState(false) + + useEffect(() => { + if (!debugChatScroll) return + const interval = setInterval(() => { + setRightNowAtBottom(isAtBottom()) + }, 50) + return () => clearInterval(interval) + }, [debugChatScroll]) const setSendHistory = (newHistory: string[]) => { sendHistoryRef.current = newHistory @@ -252,6 +263,55 @@ export default ({ }} > {opacity &&
+ {debugChatScroll && ( +
+
+
+
+
+
+ )} {messages.map((m) => ( ))} diff --git a/src/react/ChatProvider.tsx b/src/react/ChatProvider.tsx index 50912820..5c499029 100644 --- a/src/react/ChatProvider.tsx +++ b/src/react/ChatProvider.tsx @@ -8,7 +8,7 @@ import { viewerVersionState } from '../viewerConnector' import Chat, { Message, fadeMessage } from './Chat' import { useIsModalActive } from './utilsApp' import { hideNotification, notificationProxy, showNotification } from './NotificationProvider' -import { updateLoadedServerData } from './serversStorage' +import { getServerIndex, updateLoadedServerData } from './serversStorage' import { lastConnectOptions } from './AppStatusProvider' import { showOptionsModal } from './SelectOption' @@ -17,7 +17,7 @@ export default () => { const isChatActive = useIsModalActive('chat') const lastMessageId = useRef(0) const usingTouch = useSnapshot(miscUiState).currentTouch - const { chatSelect, messagesLimit, chatOpacity, chatOpacityOpened, chatVanillaRestrictions } = useSnapshot(options) + const { chatSelect, messagesLimit, chatOpacity, chatOpacityOpened, chatVanillaRestrictions, debugChatScroll } = useSnapshot(options) const isUsingMicrosoftAuth = useMemo(() => !!lastConnectOptions.value?.authenticatedAccount, []) const { forwardChat } = useSnapshot(viewerVersionState) const { viewerConnection } = useSnapshot(gameAdditionalState) @@ -48,6 +48,7 @@ export default () => { return { placeholder={forwardChat || !viewerConnection ? undefined : 'Chat forwarding is not enabled in the plugin settings'} sendMessage={async (message) => { const builtinHandled = tryHandleBuiltinCommand(message) - if (miscUiState.loadedServerIndex && (message.startsWith('/login') || message.startsWith('/register'))) { + if (getServerIndex() !== undefined && (message.startsWith('/login') || message.startsWith('/register'))) { showNotification('Click here to save your password in browser for auto-login', undefined, false, undefined, () => { updateLoadedServerData((server) => { server.autoLogin ??= {} const password = message.split(' ')[1] server.autoLogin[bot.username] = password - return server + return { ...server } }) hideNotification() }) diff --git a/src/react/ChunksDebugScreen.tsx b/src/react/ChunksDebugScreen.tsx index 80e926d8..9f47d023 100644 --- a/src/react/ChunksDebugScreen.tsx +++ b/src/react/ChunksDebugScreen.tsx @@ -30,7 +30,7 @@ const Inner = () => { state, lines: [String(chunk?.loads.length ?? 0)], sidebarLines: [ - `loads: ${chunk.loads.map(l => `${l.reason} ${l.dataLength} ${l.time}`).join('\n')}`, + `loads: ${chunk.loads?.map(l => `${l.reason} ${l.dataLength} ${l.time}`).join('\n')}`, // `blockUpdates: ${chunk.blockUpdates}`, ], } diff --git a/src/react/CreditsAboutModal.module.css b/src/react/CreditsAboutModal.module.css new file mode 100644 index 00000000..753d0448 --- /dev/null +++ b/src/react/CreditsAboutModal.module.css @@ -0,0 +1,84 @@ +.modalScreen { + margin-top: -15px; +} + +.container { + position: relative; + background-color: #eee0c3; + border: 5px solid #7A5C3E; + padding: 15px; + width: 80%; + margin: 0 auto; + color: #3F2A14; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); + max-height: 70vh; + overflow-y: auto; +} + +.title { + text-align: center; + margin-top: 0; + margin-bottom: 0; + font-size: 10px; +} + +.contentWrapper { + margin-bottom: 5px; +} + +.subtitle { + font-size: 6px; + margin-bottom: 4px; + font-style: italic; +} + +.paragraph { + font-size: 6px; + margin-bottom: 4px; +} + +.list { + list-style-type: none; + padding: 0; + font-size: 6px; +} + +.listItem { + margin-bottom: 2px; +} + +.link { + color: #0000AA; + text-decoration: none; +} + +.sectionTitle { + margin-top: 7px; + margin-bottom: 5px; + font-size: 8px; +} + +.closeButton { + position: absolute; + top: 1px; + right: 1px; + display: flex; + justify-content: center; + cursor: pointer; + padding: 5px; + background: none; + border: none; + outline: none; +} + +.closeButton:hover { + opacity: 0.8; +} + +.closeButton:focus-visible { + outline: 1px dashed #7A5C3E; +} + +.closeIcon { + color: #3F2A14; +} \ No newline at end of file diff --git a/src/react/CreditsAboutModal.module.css.d.ts b/src/react/CreditsAboutModal.module.css.d.ts new file mode 100644 index 00000000..30ac3261 --- /dev/null +++ b/src/react/CreditsAboutModal.module.css.d.ts @@ -0,0 +1,18 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + closeButton: string; + closeIcon: string; + container: string; + contentWrapper: string; + link: string; + list: string; + listItem: string; + modalScreen: string; + paragraph: string; + sectionTitle: string; + subtitle: string; + title: string; +} +declare const cssExports: CssExports; +export default cssExports; diff --git a/src/react/CreditsAboutModal.tsx b/src/react/CreditsAboutModal.tsx new file mode 100644 index 00000000..24826cd3 --- /dev/null +++ b/src/react/CreditsAboutModal.tsx @@ -0,0 +1,57 @@ +import { hideCurrentModal } from '../globalState' +import { useIsModalActive } from './utilsApp' +import Screen from './Screen' +import PixelartIcon, { pixelartIcons } from './PixelartIcon' +import styles from './CreditsAboutModal.module.css' + +export default () => { + const isModalActive = useIsModalActive('credits-about') + + if (!isModalActive) return null + + return ( + +
+

Minecraft Open Source JS Edition

+ +
+ What if Minecraft was an online game? +

+ Hey! You are on the safest modern Minecraft clone rewritten in JavaScript. A huge amount of work has gone into this project to make it fast and complete, and many features would not be possible without these awesome people and projects: +

+
    +
  • - Everyone who provided awesome mods for the game
  • +
  • - [Gen] for rewriting the physics engine to be Grim-compliant
  • +
  • - [ViaVersion] for providing reliable sound id mappings
  • +
  • - [Bluemap] for providing block entity models like chest
  • +
  • - [Deepslate] for rendering 3d blocks in GUI (inventory)
  • +
  • - [skinview3d] for rendering skins & player geometry
  • +
  • - [Polymer] (c++ project) for providing fast & accurate server light implementation
  • +
+ +

Current contributors:

+
    +
  • - Maxim Grigorev - React UI & Core Developer Maintainer
  • +
  • - And many more community contributors!
  • +
+ + +
+
+
+ ) +} diff --git a/src/react/CreditsBookButton.module.css b/src/react/CreditsBookButton.module.css new file mode 100644 index 00000000..83af5d80 --- /dev/null +++ b/src/react/CreditsBookButton.module.css @@ -0,0 +1,27 @@ +.creditsButton { + position: absolute; + top: 1px; + right: -30px; + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none; + cursor: pointer; + color: white; + opacity: 1; + transition: opacity 0.2s ease; +} + +.creditsButton:hover { + opacity: 0.9; +} + +.creditsButton:focus:not(:hover) { + outline: 1px solid #fff; +} + +.creditsButton svg { + width: 15px; + height: 15px; +} diff --git a/src/react/CreditsBookButton.module.css.d.ts b/src/react/CreditsBookButton.module.css.d.ts new file mode 100644 index 00000000..72f94435 --- /dev/null +++ b/src/react/CreditsBookButton.module.css.d.ts @@ -0,0 +1,7 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + creditsButton: string; +} +declare const cssExports: CssExports; +export default cssExports; diff --git a/src/react/CreditsBookButton.tsx b/src/react/CreditsBookButton.tsx new file mode 100644 index 00000000..a3be740f --- /dev/null +++ b/src/react/CreditsBookButton.tsx @@ -0,0 +1,22 @@ +import { showModal } from '../globalState' +import styles from './CreditsBookButton.module.css' + +export default () => { + const handleClick = () => { + showModal({ reactType: 'credits-about' }) + } + + return ( + + ) +} diff --git a/src/react/DeathScreen.tsx b/src/react/DeathScreen.tsx index 8f4c3f00..3501368f 100644 --- a/src/react/DeathScreen.tsx +++ b/src/react/DeathScreen.tsx @@ -24,7 +24,7 @@ export default ({ dieReasonMessage, respawnCallback, disconnectCallback }: Props }} />
) } diff --git a/src/react/button.module.css b/src/react/button.module.css index ae32ee5f..677a1b44 100644 --- a/src/react/button.module.css +++ b/src/react/button.module.css @@ -3,7 +3,7 @@ --txrV: 66px; position: relative; width: 200px; - height: calc(20px * var(--scale)); + min-height: calc(20px * var(--scale)); font-family: minecraft, mojangles, monospace; font-size: 10px; color: white; diff --git a/src/react/hooks/useScrollBehavior.ts b/src/react/hooks/useScrollBehavior.ts index 3b9499a6..5a839066 100644 --- a/src/react/hooks/useScrollBehavior.ts +++ b/src/react/hooks/useScrollBehavior.ts @@ -1,4 +1,4 @@ -import { RefObject, useEffect, useLayoutEffect, useRef } from 'react' +import { RefObject, useEffect, useLayoutEffect, useRef, useState } from 'react' import { pixelartIcons } from '../PixelartIcon' export const useScrollBehavior = ( @@ -12,6 +12,8 @@ export const useScrollBehavior = ( } ) => { const openedWasAtBottom = useRef(true) // before new messages + const [currentlyAtBottom, setCurrentlyAtBottom] = useState(true) + const scrollTimeoutRef = useRef(null) const isAtBottom = () => { if (!elementRef.current) return true @@ -20,17 +22,30 @@ export const useScrollBehavior = ( return distanceFromBottom < 1 } - const scrollToBottom = () => { - if (elementRef.current) { - elementRef.current.scrollTop = elementRef.current.scrollHeight - setTimeout(() => { - if (!elementRef.current) return - elementRef.current.scrollTo({ - top: elementRef.current.scrollHeight, - behavior: 'instant' - }) - }, 0) + const scrollToBottom = (behavior: ScrollBehavior = 'instant') => { + if (!elementRef.current) return + + // Clear any existing scroll timeout + if (scrollTimeoutRef.current) { + clearTimeout(scrollTimeoutRef.current) } + + const el = elementRef.current + + // Immediate scroll + el.scrollTop = el.scrollHeight + + // Double-check after a short delay to ensure we're really at the bottom + scrollTimeoutRef.current = setTimeout(() => { + if (!elementRef.current) return + const el = elementRef.current + el.scrollTo({ + top: el.scrollHeight, + behavior + }) + setCurrentlyAtBottom(true) + openedWasAtBottom.current = true + }, 5) } // Handle scroll position tracking @@ -39,18 +54,28 @@ export const useScrollBehavior = ( if (!element) return const handleScroll = () => { - openedWasAtBottom.current = isAtBottom() + const atBottom = isAtBottom() + openedWasAtBottom.current = atBottom + setCurrentlyAtBottom(atBottom) } element.addEventListener('scroll', handleScroll) - return () => element.removeEventListener('scroll', handleScroll) + return () => { + element.removeEventListener('scroll', handleScroll) + if (scrollTimeoutRef.current) { + clearTimeout(scrollTimeoutRef.current) + } + } }, []) // Handle opened state changes useLayoutEffect(() => { if (opened) { - openedWasAtBottom.current = true - } else { + // Wait a frame before scrolling to ensure DOM has updated + requestAnimationFrame(() => { + scrollToBottom() + }) + } else if (elementRef.current) { scrollToBottom() } }, [opened]) @@ -64,6 +89,8 @@ export const useScrollBehavior = ( return { scrollToBottom, - isAtBottom + isAtBottom, + wasAtBottom: () => openedWasAtBottom.current, + currentlyAtBottom } } diff --git a/src/react/serversStorage.ts b/src/react/serversStorage.ts index b320e2f3..9b095454 100644 --- a/src/react/serversStorage.ts +++ b/src/react/serversStorage.ts @@ -1,6 +1,7 @@ import { appQueryParams } from '../appParams' import { miscUiState } from '../globalState' import { BaseServerInfo } from './AddServerOrConnect' +import { lastConnectOptions } from './AppStatusProvider' import { appStorage, StoreServerItem } from './appStorageProvider' const serversListQs = appQueryParams.serversList @@ -43,9 +44,34 @@ export function updateServerConnectionHistory (ip: string, version?: string) { } } -export const updateLoadedServerData = (callback: (data: StoreServerItem) => StoreServerItem, index = miscUiState.loadedServerIndex) => { - if (index === undefined) index = miscUiState.loadedServerIndex +export const getServerIndex = () => { + const lastConnectedIp = lastConnectOptions.value?.server + const index = miscUiState.loadedServerIndex + if (index !== undefined) return index + if (lastConnectedIp) { + const idx = appStorage.serversList?.findIndex(s => s.ip === lastConnectedIp).toString() + if (idx === '-1') return undefined + return idx + } + return undefined +} + +export const findServerPassword = () => { + const { username } = bot + const index = getServerIndex() if (index === undefined) return + const pswd = appStorage.serversList?.[index]?.autoLogin?.[username] + if (pswd) return pswd + // try other servers with same host + return appStorage.serversList?.find(s => s.ip === lastConnectOptions.value?.server && s.autoLogin?.[username])?.autoLogin?.[username] +} + +export const updateLoadedServerData = (callback: (data: StoreServerItem) => StoreServerItem, index = miscUiState.loadedServerIndex) => { + if (index === undefined) { + const idx = getServerIndex() + if (idx === undefined) return + index = idx + } const servers = [...(appStorage.serversList ?? [])] const server = servers[index] diff --git a/src/reactUi.tsx b/src/reactUi.tsx index d2633708..e67facea 100644 --- a/src/reactUi.tsx +++ b/src/reactUi.tsx @@ -62,6 +62,8 @@ import ChunksDebug from './react/ChunksDebug' import ChunksDebugScreen from './react/ChunksDebugScreen' import DebugResponseTimeIndicator from './react/debugs/DebugResponseTimeIndicator' import RendererDebugMenu from './react/RendererDebugMenu' +import CreditsAboutModal from './react/CreditsAboutModal' +import GlobalOverlayHints from './react/GlobalOverlayHints' const isFirefox = ua.getBrowser().name === 'Firefox' if (isFirefox) { @@ -175,6 +177,7 @@ const InGameUi = () => { {showUI && } +
{!disabledUiParts.includes('xp-bar') && } {!disabledUiParts.includes('hud-bars') && } @@ -241,7 +244,7 @@ const App = () => { - + diff --git a/src/utils.ts b/src/utils.ts index 93e2eb7b..d48fbbc3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,6 @@ import { gameAdditionalState, isGameActive, miscUiState } from './globalState' import { options } from './optionsStorage' +import { displayHintsState } from './react/GlobalOverlayHints' import { notificationProxy, showNotification } from './react/NotificationProvider' import { packetsReplayState } from './react/state/packetsReplayState' @@ -38,13 +39,14 @@ export const pointerLock = { if (options.autoFullScreen) { void goFullscreen() } - const displayBrowserProblem = () => { - if (notificationProxy.id === 'auto-login') return // prevent notification hide - showNotification('Browser Delay Limitation', navigator['keyboard'] ? 'Click on screen, enable Auto Fullscreen or F11' : 'Click on screen or use fullscreen in Chrome') - notificationProxy.id = 'pointerlockchange' + const displayMouseCaptureFailure = () => { + // if (notificationProxy.id === 'auto-login') return // prevent notification hide + // showNotification('Browser Delay Limitation', navigator['keyboard'] ? 'Click on screen, enable Auto Fullscreen or F11' : 'Click on screen or use fullscreen in Chrome') + // notificationProxy.id = 'pointerlockchange' + displayHintsState.captureMouseHint = true } if (!(document.fullscreenElement && navigator['keyboard']) && this.justHitEscape) { - displayBrowserProblem() + displayMouseCaptureFailure() } else { //@ts-expect-error const promise: any = document.documentElement.requestPointerLock({ @@ -56,9 +58,10 @@ export const pointerLock = { document.documentElement.requestPointerLock() } else if (error.name === 'SecurityError') { // cause: https://discourse.threejs.org/t/how-to-avoid-pointerlockcontrols-error/33017/4 - displayBrowserProblem() + displayMouseCaptureFailure() } else { - console.error(error) + displayMouseCaptureFailure() + console.warn('Failed to request pointer lock:', error) } }) } diff --git a/src/utils/splashText.ts b/src/utils/splashText.ts new file mode 100644 index 00000000..4052c4f4 --- /dev/null +++ b/src/utils/splashText.ts @@ -0,0 +1,67 @@ +const MAX_WORDS = 5 +const HTTPS_REGEX = /^https?:\/\// +const TIMEOUT_MS = 5000 +const SPLASH_CACHE_KEY = 'minecraft_splash_text_cache' +const SPLASH_URL_KEY = 'minecraft_splash_url' + +const limitWords = (text: string): string => { + const words = text.split(/\s+/) + if (words.length <= MAX_WORDS) { + return text + } + return words.slice(0, MAX_WORDS).join(' ') + '...' +} + +export const isRemoteSplashText = (text: string): boolean => { + if (!text) return false + return HTTPS_REGEX.test(text) +} + +export const loadRemoteSplashText = async (url: string): Promise => { + try { + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS) + const response = await fetch(url, { signal: controller.signal }) + clearTimeout(timeoutId) + if (!response.ok) { + throw new Error(`Failed to fetch splash text: ${response.statusText}`) + } + + const clonedResponse = response.clone() + try { + const json = await response.json() + + if (typeof json === 'object' && json !== null) { + if (json.title) return limitWords(json.title) + if (json.text) return limitWords(json.text) + if (json.message) return limitWords(json.message) + + return limitWords(JSON.stringify(json)) + } + + return limitWords(String(json)) + } catch (jsonError) { + const text = await clonedResponse.text() + return limitWords(text.trim()) + } + } catch (error) { + console.error('Error loading remote splash text:', error) + return 'Failed to load splash text!' + } +} + +export const cacheSourceUrl = (url: string): void => { + localStorage.setItem(SPLASH_URL_KEY, url) +} + +export const clearSplashCache = (): void => { + localStorage.removeItem(SPLASH_CACHE_KEY) +} + +export const getCachedSplashText = (): string | null => { + return localStorage.getItem(SPLASH_CACHE_KEY) +} + +export const cacheSplashText = (text: string): void => { + localStorage.setItem(SPLASH_CACHE_KEY, text) +} diff --git a/src/watchOptions.ts b/src/watchOptions.ts index 9fe55289..903d7da8 100644 --- a/src/watchOptions.ts +++ b/src/watchOptions.ts @@ -80,6 +80,11 @@ export const watchOptionsAfterViewerInit = () => { updateFpsLimit(o) }) + watchValue(options, o => { + appViewer.inWorldRenderingConfig.vrSupport = o.vrSupport + appViewer.inWorldRenderingConfig.vrPageGameRendering = o.vrPageGameRendering + }) + watchValue(options, (o, isChanged) => { appViewer.inWorldRenderingConfig.clipWorldBelowY = o.clipWorldBelowY appViewer.inWorldRenderingConfig.extraBlockRenderers = !o.disableSignsMapsSupport