Merge branch 'next' into renderer-debug

This commit is contained in:
Vitaly Turovsky 2025-06-05 16:48:46 +03:00
commit c151e414d9
74 changed files with 1492 additions and 418 deletions

View file

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

View file

@ -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": ""
}
]
}

View file

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

View file

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

194
pnpm-lock.yaml generated
View file

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

View file

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

View file

@ -31,6 +31,8 @@ export interface IPlayerState {
getHeldItem?(isLeftHand: boolean): HandItemBlock | undefined
username?: string
onlineMode?: boolean
lightingDisabled?: boolean
shouldHideHand?: boolean
events: TypedEmitter<PlayerStateEvents>

View file

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

View file

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

View file

@ -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<string, any>,
signs: Record<string, any>,
// isFull: boolean
highestBlocks: Record<string, HighestBlockInfo>
highestBlocks: Map<string, HighestBlockInfo>
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<string, BlockStateModelInfo> };
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 = {

View file

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

View file

@ -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<WorkerSend = any, WorkerReceive = any>
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<WorkerSend = any, WorkerReceive = any>
ONMESSAGE_TIME_LIMIT = 30 // ms
handleResize = () => { }
highestBlocksByChunks = {} as Record<string, { [chunkKey: string]: HighestBlockInfo }>
highestBlocksBySections = {} as Record<string, { [sectionKey: string]: HighestBlockInfo }>
highestBlocksByChunks = new Map<string, { [chunkKey: string]: HighestBlockInfo }>()
highestBlocksBySections = new Map<string, { [sectionKey: string]: HighestBlockInfo }>()
blockEntities = {}
workersProcessAverageTime = 0
@ -259,7 +261,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
}
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<WorkerSend = any, WorkerReceive = any>
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<WorkerSend = any, WorkerReceive = any>
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<WorkerSend = any, WorkerReceive = any>
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<WorkerSend = any, WorkerReceive = any>
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<WorkerSend = any, WorkerReceive = any>
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<WorkerSend = any, WorkerReceive = any>
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<WorkerSend = any, WorkerReceive = any>
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<WorkerSend = any, WorkerReceive = any>
this.active = false
this.renderUpdateEmitter.removeAllListeners()
this.displayOptions.worldView.removeAllListeners() // todo
this.abortController.abort()
removeAllStats()
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<string, boolean>
allowAutoConnect?: boolean
splashText?: string
splashTextFallback?: string
pauseLinks?: Array<Array<Record<string, any>>>
mobileButtons?: MobileButtonConfig[]
keybindings?: Record<string, any>
defaultLanguage?: string
displayLanguageSelector?: boolean

View file

@ -17,7 +17,8 @@ import { watchOptionsAfterWorldViewInit } from './watchOptions'
export interface RendererReactiveState {
world: {
chunksLoaded: string[]
chunksLoaded: Set<string>
heightmaps: Map<string, Uint8Array>
chunksTotalNumber: number
allChunksLoaded: boolean
mesherWork: boolean
@ -28,7 +29,7 @@ export interface RendererReactiveState {
}
export interface NonReactiveState {
world: {
chunksLoaded: string[]
chunksLoaded: Set<string>
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)
}
}

View file

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

View file

@ -21,7 +21,6 @@ export type ConnectOptions = {
peerId?: string
ignoreQs?: boolean
onSuccessfulPlay?: () => void
autoLoginPassword?: string
serverIndex?: string
authenticatedAccount?: AuthenticatedAccount | true
peerOptions?: any

View file

@ -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<string>()
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<typeof contro['_commandsRaw']> = {
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)
}
}

View file

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

View file

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

View file

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

View file

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

6
src/globals.d.ts vendored
View file

@ -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: <T extends string | undefined>(key: T) => T
// API LAYER
declare const toggleMicrophoneMuted: undefined | (() => void)
declare const translateText: undefined | ((text: string) => string)
declare interface Document {
exitPointerLock?(): void

View file

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

View file

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

View file

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

View file

@ -25,6 +25,7 @@ import { GeneralInputItem, getItemMetadata, getItemModelName, getItemNameRaw, Re
const loadedImagesCache = new Map<string, HTMLImageElement>()
const cleanLoadedImagesCache = () => {
loadedImagesCache.delete('blocks')
loadedImagesCache.delete('items')
}
let lastWindow: ReturnType<typeof showInventory>
@ -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',

View file

@ -92,7 +92,8 @@ export const getItemMetadata = (item: GeneralInputItem, resourcesManager: Resour
export const getItemNameRaw = (item: Pick<import('prismarine-item').Item, 'nbt'> | 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') {

View file

@ -3,13 +3,11 @@ import clientAutoVersion from 'minecraft-protocol/src/client/autoVersion'
export const pingServerVersion = async (ip: string, port?: number, mergeOptions: Record<string, any> = {}) => {
const fakeClient = new EventEmitter() as any
fakeClient.on('error', (err) => {
throw new Error(err.message ?? err)
})
const options = {
host: ip,
port,
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<void>((resolve, reject) => {
fakeClient.once('connect_allowed', () => {
resolve()
})
}),
new Promise<void>((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<void>((resolve, reject) => {
fakeClient.once('connect_allowed', resolve)
})
return {
version: fakeClient.version,
latency,

View file

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

View file

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

View file

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

View file

@ -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 <Category>Map</Category>
},
showMinimap: {
text: 'Enable Minimap',
enableWarning: 'App reload is required to apply this setting',
values: [
'always',
'singleplayer',
'never'
],
},
},
{
custom () {
return <Category>World</Category>
@ -321,19 +334,6 @@ export const guiOptionsScheme: {
],
},
},
{
custom () {
return <Category>Map</Category>
},
showMinimap: {
text: 'Enable Minimap',
values: [
'always',
'singleplayer',
'never'
],
},
},
{
custom () {
return <Category>Experimental</Category>
@ -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': [

View file

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

View file

@ -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 <Screen title={qsParamIp ? 'Connect to Server' : title} backdrop>
<form
@ -149,11 +151,6 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
})
}}
>
{!lockConnect && <>
<div style={{ gridColumn: smallWidth ? '' : 'span 2', display: 'flex', justifyContent: 'center' }}>
<InputWithLabel label="Server Name" value={serverName} onChange={({ target: { value } }) => setServerName(value)} placeholder='Defaults to IP' />
</div>
</>}
<InputWithLabel
required
label="Server IP"
@ -165,8 +162,13 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
setServerOnline(false)
}}
validateInput={serverOnline === null || fetchedServerInfoIp !== serverIp ? undefined : validateServerIp}
placeholder={example}
/>
<InputWithLabel label="Server Port" value={serverPort} disabled={lockConnect && parsedQsIp.port !== null} onChange={({ target: { value } }) => setServerPort(value)} placeholder={serverIp.startsWith('ws://') || serverIp.startsWith('wss://') ? '' : '25565'} />
{!lockConnect && <>
<div style={{ display: 'flex' }}>
<InputWithLabel label="Server Name" value={serverName} onChange={({ target: { value } }) => setServerName(value)} placeholder='Defaults to IP' />
</div>
</>}
{isSmallHeight ? <div style={{ gridColumn: 'span 2', marginTop: 10, }} /> : <div style={{ gridColumn: smallWidth ? '' : 'span 2' }}>Overrides:</div>}
<div style={{
display: 'flex',

View file

@ -41,6 +41,7 @@ type Props = {
inputDisabled?: string
placeholder?: string
chatVanillaRestrictions?: boolean
debugChatScroll?: boolean
}
export const chatInputValueGlobal = proxy({
@ -69,7 +70,8 @@ export default ({
allowSelection,
inputDisabled,
placeholder,
chatVanillaRestrictions
chatVanillaRestrictions,
debugChatScroll
}: Props) => {
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 && <div ref={chatMessages} className={`chat ${opened ? 'opened' : ''}`} id="chat-messages" style={{ opacity }}>
{debugChatScroll && (
<div
style={{
position: 'absolute',
top: 5,
left: 5,
display: 'flex',
gap: 4,
zIndex: 100,
}}
>
<div
title="Right now is at bottom (updated every 50ms)"
style={{
width: 12,
height: 12,
backgroundColor: rightNowAtBottom ? '#00ff00' : '#ff0000',
border: '1px solid #fff',
}}
/>
<div
title="Currently at bottom"
style={{
width: 12,
height: 12,
backgroundColor: currentlyAtBottom ? '#00ff00' : '#ff0000',
border: '1px solid #fff',
}}
/>
<div
title="Was at bottom"
style={{
width: 12,
height: 12,
backgroundColor: wasAtBottom() ? '#00ff00' : '#ff0000',
border: '1px solid #fff',
}}
/>
<div
title="Chat opened"
style={{
width: 12,
height: 12,
backgroundColor: opened ? '#00ff00' : '#ff0000',
border: '1px solid #fff',
}}
/>
</div>
)}
{messages.map((m) => (
<MessageLine key={reactKeyForMessage(m)} message={m} />
))}

View file

@ -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 <Chat
chatVanillaRestrictions={chatVanillaRestrictions}
debugChatScroll={debugChatScroll}
allowSelection={chatSelect}
usingTouch={!!usingTouch}
opacity={(isChatActive ? chatOpacityOpened : chatOpacity) / 100}
@ -56,13 +57,13 @@ export default () => {
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()
})

View file

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

View file

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

View file

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

View file

@ -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 (
<Screen
title=""
backdrop
className={styles.modalScreen}>
<div className={styles.container}>
<h2 className={styles.title}>Minecraft Open Source JS Edition</h2>
<div className={styles.contentWrapper}>
<small className={styles.subtitle}><i>What if Minecraft was an online game?</i></small>
<p className={styles.paragraph}>
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:
</p>
<ul className={styles.list}>
<li className={styles.listItem}>- Everyone who provided awesome mods for the game</li>
<li className={styles.listItem}>- <span><a className={styles.link} href="https://discord.com/users/gen6442" target="_blank" rel="noopener noreferrer">[Gen]</a></span> for rewriting the physics engine to be Grim-compliant</li>
<li className={styles.listItem}>- <span><a className={styles.link} href="https://viaversion.com/" target="_blank" rel="noopener noreferrer">[ViaVersion]</a></span> for providing reliable sound id mappings</li>
<li className={styles.listItem}>- <span><a className={styles.link} href="https://github.com/BlueMap-Minecraft/BlueMap" target="_blank" rel="noopener noreferrer">[Bluemap]</a></span> for providing block entity models like chest</li>
<li className={styles.listItem}>- <span><a className={styles.link} href="https://github.com/misode/deepslate" target="_blank" rel="noopener noreferrer">[Deepslate]</a></span> for rendering 3d blocks in GUI (inventory)</li>
<li className={styles.listItem}>- <span><a className={styles.link} href="https://www.npmjs.com/package/skinview3d" target="_blank" rel="noopener noreferrer">[skinview3d]</a></span> for rendering skins & player geometry</li>
<li className={styles.listItem}>- <span><a className={styles.link} href="https://github.com/atxi/Polymer" target="_blank" rel="noopener noreferrer">[Polymer]</a></span> (c++ project) for providing fast & accurate server light implementation</li>
</ul>
<h3 className={styles.sectionTitle}>Current contributors:</h3>
<ul className={styles.list}>
<li className={styles.listItem}>- <span className={styles.link}><a className={styles.link} href="https://github.com/mgDentist" target="_blank" rel="noopener noreferrer">Maxim Grigorev</a></span> - React UI & Core Developer Maintainer</li>
<li className={styles.listItem}>- And many more community contributors!</li>
</ul>
<button
className={styles.closeButton}
onClick={() => hideCurrentModal()}
aria-label="Close"
type="button"
>
<PixelartIcon
iconName={pixelartIcons.close}
width={12}
className={styles.closeIcon}
/>
</button>
</div>
</div>
</Screen>
)
}

View file

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

View file

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

View file

@ -0,0 +1,22 @@
import { showModal } from '../globalState'
import styles from './CreditsBookButton.module.css'
export default () => {
const handleClick = () => {
showModal({ reactType: 'credits-about' })
}
return (
<button
className={styles.creditsButton}
onClick={handleClick}
aria-label="Credits"
title="Credits"
>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.4375 15.1238L15.625 15.3113V16.8113L15.4375 16.9988H14.6875L14.5 16.8113V16.4363L14.3125 16.2488H12.8125L12.625 16.0613V15.3113L12.4375 15.1238H10.9375L10.75 14.9363V14.1863L10.5625 13.9988H7.9375L7.75 14.1863V14.9363L7.5625 15.1238H6.0625L5.875 15.3113V16.0613L5.6875 16.2488H4.1875L4 16.4363V16.8113L3.8125 16.9988H3.0625L2.875 16.8113V15.3113L3.0625 15.1238H3.4375L3.625 14.9363V13.8113L3.8125 13.6238H4.1875L4.375 13.4363V11.9363L4.5625 11.7488H4.9375L5.125 11.5613V10.0613L4.9375 9.87384H4.1875L4 9.68634V8.93634L3.8125 8.74884H2.6875L2.5 8.56134V7.81134L2.3125 7.62384H1.1875L1 7.43634V6.68634L1.1875 6.49884H6.63738L6.82488 6.31134V5.61543V4.60586L7.01238 4.41836H7.5625L7.75 4.23086V2.6875L7.9375 2.5H8.43022L8.61772 2.3125V1.1875L8.80522 1H9.66735L9.85485 1.1875V2.3125L10.0424 2.5H10.5625L10.75 2.6875V4.23086L10.9375 4.41836H11.4375L11.625 4.60586V6.31134L11.8125 6.49884H17.3125L17.5 6.68634V7.43634L17.3125 7.62384H16.1875L16 7.81134V8.56134L15.8125 8.74884H14.6875L14.5 8.93634V9.68634L14.3125 9.87384H13.5625L13.375 10.0613V11.5613L13.5625 11.7488H13.9375L14.125 11.9363V13.4363L14.3125 13.6238H14.6875L14.875 13.8113V14.9363L15.0625 15.1238H15.4375Z" fill="#FFD700" />
</svg>
</button>
)
}

View file

@ -24,7 +24,7 @@ export default ({ dieReasonMessage, respawnCallback, disconnectCallback }: Props
}}
/>
<Button
label="Disconnnect" onClick={() => {
label="Disconnect" onClick={() => {
disconnectCallback()
}}
/>

View file

@ -1,6 +1,8 @@
import { useEffect, useRef, useState } from 'react'
import { useSnapshot } from 'valtio'
import type { Block } from 'prismarine-block'
import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods'
import { miscUiState } from '../globalState'
import { getFixedFilesize } from '../downloadAndOpenFile'
import { options } from '../optionsStorage'
import { BlockStateModelInfo } from '../../renderer/viewer/lib/mesher/shared'
@ -28,7 +30,7 @@ export default () => {
window.packetsCountByName = packetsCountByName
const ignoredPackets = useRef(new Set([] as any[]))
const [packetsString, setPacketsString] = useState('')
const [showDebug, setShowDebug] = useState(false)
const { showDebugHud } = useSnapshot(miscUiState)
const [pos, setPos] = useState<{ x: number, y: number, z: number }>({ x: 0, y: 0, z: 0 })
const [skyL, setSkyL] = useState(0)
const [blockL, setBlockL] = useState(0)
@ -53,13 +55,6 @@ export default () => {
const viewDegToMinecraft = (yaw) => yaw % 360 - 180 * (yaw < 0 ? -1 : 1)
const handleF3 = (e) => {
if (e.code === 'F3') {
setShowDebug(prev => !prev)
e.preventDefault()
}
}
const readPacket = (data, { name }, _buf, fullBuffer) => {
if (fullBuffer) {
const size = fullBuffer.byteLength
@ -105,7 +100,6 @@ export default () => {
}
}
document.addEventListener('keydown', handleF3)
let update = 0
const packetsUpdateInterval = setInterval(() => {
setPacketsString(`${received.current.count} (${(received.current.size / 1024).toFixed(2)} KB/s, ${getFixedFilesize(receivedTotal.current)}) ↑ ${sent.current.count}`)
@ -161,7 +155,6 @@ export default () => {
})
return () => {
document.removeEventListener('keydown', handleF3)
clearInterval(packetsUpdateInterval)
clearInterval(freqUpdateInterval)
clearInterval(notFrequentUpdateInterval)
@ -174,7 +167,7 @@ export default () => {
minecraftQuad.current = Math.floor(((minecraftYaw.current + 180) / 90 + 0.5) % 4)
}, [bot.entity.yaw])
if (!showDebug) return null
if (!showDebugHud) return null
return <>
<div className={`debug-left-side ${styles['debug-left-side']}`}>

View file

@ -0,0 +1,56 @@
import { useEffect } from 'react'
import { proxy, useSnapshot } from 'valtio'
import { isInRealGameSession, pointerLock } from '../utils'
import { activeModalStack, miscUiState } from '../globalState'
import PixelartIcon, { pixelartIcons } from './PixelartIcon'
import { useUsingTouch } from './utilsApp'
export const displayHintsState = proxy({
captureMouseHint: false
})
export default () => {
const { captureMouseHint } = useSnapshot(displayHintsState)
const { usingGamepadInput } = useSnapshot(miscUiState)
const usingTouch = useUsingTouch()
const acitveModals = useSnapshot(activeModalStack).length > 0
const inRealGameSession = isInRealGameSession()
useEffect(() => {
const listener = () => {
if (pointerLock.hasPointerLock) {
displayHintsState.captureMouseHint = false
}
}
document.addEventListener('pointerlockchange', listener)
return () => {
document.removeEventListener('pointerlockchange', listener)
}
}, [])
return <div style={{
// below crosshair that is in center of screen
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
color: 'white',
fontSize: '10px',
whiteSpace: 'nowrap',
fontWeight: 'bold',
marginTop: '-16px',
pointerEvents: 'none',
textShadow: '0 0 1px black'
}}>
{captureMouseHint && !usingTouch && !usingGamepadInput && !acitveModals && inRealGameSession && <div style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
}}>
<PixelartIcon iconName={pixelartIcons['sun-alt']} />
<div>{translate('Click to capture mouse')}</div>
</div>}
</div>
}

View file

@ -80,6 +80,9 @@ const HotbarInner = () => {
const controller = new AbortController()
const inv = openItemsCanvas('HotbarWin', {
_client: {
write () {}
},
clickWindow (slot, mouseButton, mode) {
if (mouseButton === 1) {
console.log('right click')

View file

@ -1,15 +1,24 @@
import React from 'react'
import React, { useEffect, useMemo } from 'react'
import { openURL } from 'renderer/viewer/lib/simpleUtils'
import { useSnapshot } from 'valtio'
import { haveDirectoryPicker } from '../utils'
import { ConnectOptions } from '../connect'
import { miscUiState } from '../globalState'
import {
isRemoteSplashText,
loadRemoteSplashText,
getCachedSplashText,
cacheSplashText,
cacheSourceUrl,
clearSplashCache
} from '../utils/splashText'
import styles from './mainMenu.module.css'
import Button from './Button'
import ButtonWithTooltip from './ButtonWithTooltip'
import { pixelartIcons } from './PixelartIcon'
import useLongPress from './useLongPress'
import PauseLinkButtons from './PauseLinkButtons'
import CreditsBookButton from './CreditsBookButton'
type Action = (e: React.MouseEvent<HTMLButtonElement>) => void
@ -44,10 +53,48 @@ export default ({
versionTitle,
onVersionStatusClick,
bottomRightLinks,
singleplayerAvailable = true
singleplayerAvailable = true,
}: Props) => {
const { appConfig } = useSnapshot(miscUiState)
const splashText = useMemo(() => {
const cachedText = getCachedSplashText()
const configSplashFromApp = appConfig?.splashText
const isRemote = configSplashFromApp && isRemoteSplashText(configSplashFromApp)
const sourceKey = isRemote ? configSplashFromApp : (configSplashFromApp || '')
const storedSourceKey = localStorage.getItem('minecraft_splash_url')
if (storedSourceKey !== sourceKey) {
clearSplashCache()
cacheSourceUrl(sourceKey)
} else if (cachedText) {
return cachedText
}
if (!isRemote && configSplashFromApp && configSplashFromApp.trim() !== '') {
cacheSplashText(configSplashFromApp)
return configSplashFromApp
}
return appConfig?.splashTextFallback || ''
}, [])
useEffect(() => {
const configSplashFromApp = appConfig?.splashText
if (configSplashFromApp && isRemoteSplashText(configSplashFromApp)) {
loadRemoteSplashText(configSplashFromApp)
.then(fetchedText => {
if (fetchedText && fetchedText.trim() !== '' && !fetchedText.includes('Failed to load')) {
cacheSplashText(fetchedText)
}
})
.catch(error => {
console.error('Failed to preload splash text for next session:', error)
})
}
}, [appConfig?.splashText])
if (!bottomRightLinks?.trim()) bottomRightLinks = undefined
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const linksParsed = bottomRightLinks?.split(/;|\n/g).map(l => {
@ -75,7 +122,7 @@ export default ({
const connectToServerLongPress = useLongPress(
() => {
if (process.env.NODE_ENV === 'development') {
// Connect to <origin>:25565
// Connect to <origin>:25565
const origin = window.location.hostname
const connectOptions: ConnectOptions = {
server: `${origin}:25565`,
@ -93,7 +140,7 @@ export default ({
<div className={styles['game-title']}>
<div className={styles.minecraft}>
<div className={styles.edition} />
<span className={styles.splash}>{appConfig?.splashText}</span>
<span className={styles.splash}>{splashText}</span>
</div>
</div>
@ -149,6 +196,7 @@ export default ({
<div className={styles['menu-row']}>
<PauseLinkButtons />
</div>
<CreditsBookButton />
</div>
<div className={styles['bottom-info']}>

View file

@ -22,8 +22,14 @@ export default (
const canvasTick = useRef(0)
const canvasRef = useRef<HTMLCanvasElement>(null)
const [position, setPosition] = useState({ x: 0, y: 0, z: 0 })
const lastUpdate = useRef(0)
const THROTTLE_MS = 50 // 20fps
const updateMap = () => {
const now = Date.now()
if (now - lastUpdate.current < THROTTLE_MS) return
lastUpdate.current = now
setPosition({ x: adapter.playerPosition.x, y: adapter.playerPosition.y, z: adapter.playerPosition.z })
if (adapter.mapDrawer) {
if (!full.current) {
@ -85,6 +91,7 @@ export default (
top: '0px',
padding: '5px 5px 0px 0px',
textAlign: 'center',
zIndex: 7,
}}
onClick={() => {
toggleFullMap?.()
@ -106,7 +113,7 @@ export default (
textShadow: '0.1em 0 black, 0 0.1em black, -0.1em 0 black, 0 -0.1em black, -0.1em -0.1em black, -0.1em 0.1em black, 0.1em -0.1em black, 0.1em 0.1em black'
}}
>
{position.x.toFixed(2)} {position.y.toFixed(2)} {position.z.toFixed(2)}
{Math.round(position.x)} {Math.round(position.y)} {Math.round(position.z)}
</div>
</div> : null
}

View file

@ -10,7 +10,7 @@ import { Chunk } from 'prismarine-world/types/world'
import { Block } from 'prismarine-block'
import { INVISIBLE_BLOCKS } from 'renderer/viewer/lib/mesher/worldConstants'
import { getRenamedData } from 'flying-squid/dist/blockRenames'
import { useSnapshot } from 'valtio'
import { useSnapshot, subscribe } from 'valtio'
import { subscribeKey } from 'valtio/utils'
import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods'
import BlockData from '../../renderer/viewer/lib/moreBlockDataGenerated.json'
@ -42,10 +42,10 @@ export class DrawerAdapterImpl extends TypedEventEmitter<MapUpdates> implements
yaw: number
world: string
warps: WorldWarp[] = gameAdditionalState.warps
chunksStore = new Map<string, undefined | null | 'requested' | ChunkInfo >()
chunksStore = new Map<string, undefined | null | 'requested' | ChunkInfo>()
loadingChunksQueue = new Set<string>()
loadChunk: (key: string) => Promise<void> = this.loadChunkMinimap
mapDrawer = new MinimapDrawer(this.loadChunk, this.warps, this.loadingChunksQueue, this.chunksStore)
mapDrawer = new MinimapDrawer(this.loadChunk.bind(this), this.warps, this.loadingChunksQueue, this.chunksStore)
currChunk: PCChunk | undefined
currChunkPos: { x: number, z: number } = { x: 0, z: 0 }
isOldVersion: boolean
@ -119,9 +119,9 @@ export class DrawerAdapterImpl extends TypedEventEmitter<MapUpdates> implements
this.blockData.set(renamedKey, BlockData.colors[blockKey])
}
subscribeKey(appViewer.rendererState, 'world', () => {
subscribe(appViewer.rendererState.world, () => {
for (const key of this.loadingChunksQueue) {
if (appViewer.rendererState.world.chunksLoaded.includes(key)) {
if (appViewer.rendererState.world.chunksLoaded.has(key)) {
this.loadingChunksQueue.delete(key)
void this.loadChunk(key)
}
@ -205,33 +205,31 @@ export class DrawerAdapterImpl extends TypedEventEmitter<MapUpdates> implements
const [chunkX, chunkZ] = key.split(',').map(Number)
const chunkWorldX = chunkX * 16
const chunkWorldZ = chunkZ * 16
if (appViewer.rendererState.world.chunksLoaded.includes(`${chunkWorldX},${chunkWorldZ}`)) {
const highestBlocks = await getThreeJsRendererMethods()?.getHighestBlocks(`${chunkWorldX},${chunkWorldZ}`)
if (!highestBlocks) return undefined
const heightmap = new Uint8Array(256)
if (appViewer.rendererState.world.chunksLoaded.has(key)) {
// console.log('[MinimapProvider] loading chunk for minimap', key)
const heightmap = appViewer.rendererState.world.heightmaps.get(key)
if (heightmap) {
// console.log('[MinimapProvider] did get highest blocks')
} else {
console.warn('[MinimapProvider] no highestBlocks from renderMethods')
return undefined
}
const colors = Array.from({ length: 256 }).fill('') as string[]
// avoid creating new object every time
const blockPos = new Vec3(0, 0, 0)
// filling up colors and heightmap
// filling up colors
for (let z = 0; z < 16; z += 1) {
for (let x = 0; x < 16; x += 1) {
const blockX = chunkWorldX + x
const blockZ = chunkWorldZ + z
const hBlock = highestBlocks[`${blockX},${blockZ}`]
blockPos.x = blockX; blockPos.z = blockZ; blockPos.y = hBlock?.y ?? 0
let block = bot.world.getBlock(blockPos)
while (block?.name.includes('air')) {
blockPos.y -= 1
block = bot.world.getBlock(blockPos)
}
const index = z * 16 + x
blockPos.x = blockX; blockPos.z = blockZ; blockPos.y = heightmap[index]
const block = bot.world.getBlock(blockPos)
// blocks which are not set are shown as half transparent
if (!block || !hBlock) {
heightmap[index] = 0
if (!block) {
colors[index] = 'rgba(0, 0, 0, 0.5)'
continue
}
heightmap[index] = block.position.y
colors[index] = this.setColor(block)
}
}
@ -242,6 +240,7 @@ export class DrawerAdapterImpl extends TypedEventEmitter<MapUpdates> implements
} else {
this.loadingChunksQueue.add(`${chunkX},${chunkZ}`)
this.chunksStore.set(key, 'requested')
// console.log('[MinimapProvider] requested new chunk', key)
}
}
@ -339,7 +338,7 @@ export class DrawerAdapterImpl extends TypedEventEmitter<MapUpdates> implements
const chunkWorldX = chunkX * 16
const chunkWorldZ = chunkZ * 16
const highestBlocks = await getThreeJsRendererMethods()?.getHighestBlocks(`${chunkWorldX},${chunkWorldZ}`)
if (appViewer.rendererState.world.chunksLoaded.includes(`${chunkWorldX},${chunkWorldZ}`)) {
if (appViewer.rendererState.world.chunksLoaded.has(`${chunkWorldX},${chunkWorldZ}`)) {
const heightmap = new Uint8Array(256)
const colors = Array.from({ length: 256 }).fill('') as string[]
if (!highestBlocks) return null

View file

@ -35,3 +35,17 @@
padding: 4px 6px;
outline: 0.5px solid white;
}
.tab-btn {
color: #fff;
background: #9c8c86;
outline: .5px solid #fff;
padding: 4px 6px;
font-family: pixelarticons, mojangles, monospace;
font-size: 8px;
}
.tab-btn:has(> div) {
padding: 3px 5px;
font-size: 12px;
}

View file

@ -9,6 +9,8 @@ interface CssExports {
mobileTopBtns: string;
'pause-btn': string;
pauseBtn: string;
'tab-btn': string;
tabBtn: string;
}
declare const cssExports: CssExports;
export default cssExports;

View file

@ -1,91 +1,162 @@
import { useEffect, useRef } from 'react'
import { f3Keybinds } from '../controls'
import { useSnapshot } from 'valtio'
import { handleMobileButtonActionCommand, handleMobileButtonLongPress } from '../controls'
import { watchValue } from '../optionsStorage'
import { showModal, miscUiState, activeModalStack, hideCurrentModal } from '../globalState'
import { showOptionsModal } from './SelectOption'
import useLongPress from './useLongPress'
import { type MobileButtonConfig, type ActionHoldConfig, type ActionType, type CustomAction } from '../appConfig'
import { miscUiState } from '../globalState'
import PixelartIcon from './PixelartIcon'
import styles from './MobileTopButtons.module.css'
export default () => {
const elRef = useRef<HTMLDivElement | null>(null)
const { appConfig } = useSnapshot(miscUiState)
const mobileButtonsConfig = appConfig?.mobileButtons
const showMobileControls = (bl) => {
if (elRef.current) elRef.current.style.display = bl ? 'flex' : 'none'
const longPressTimerIdRef = useRef<number | null>(null)
const actionToShortPressRef = useRef<ActionType | null>(null)
const showMobileControls = (visible: boolean) => {
if (elRef.current) {
elRef.current.style.display = visible ? 'flex' : 'none'
}
}
useEffect(() => {
watchValue(miscUiState, o => {
showMobileControls(o.currentTouch)
showMobileControls(Boolean(o.currentTouch))
})
}, [])
const onLongPress = 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()
}
const getButtonClassName = (button: MobileButtonConfig): string => {
const actionForStyle = button.action || (button.actionHold && typeof button.actionHold === 'object' && 'command' in button.actionHold ? button.actionHold.command : undefined)
const defaultOptions = {
shouldPreventDefault: true,
delay: 500,
}
const longPressEvent = useLongPress(onLongPress, () => {}, defaultOptions)
const onChatLongPress = () => {
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }))
}
const onChatClick = () => {
if (activeModalStack.at(-1)?.reactType === 'chat') {
hideCurrentModal()
} else {
showModal({ reactType: 'chat' })
if (typeof actionForStyle === 'string') {
switch (actionForStyle) {
case 'general.chat':
return styles['chat-btn']
case 'ui.pauseMenu':
return styles['pause-btn']
case 'general.playersList':
return styles['tab-btn']
default:
return styles['debug-btn']
}
}
return styles['debug-btn']
}
const chatLongPressEvent = useLongPress(
onChatLongPress,
onChatClick,
{
shouldPreventDefault: true,
delay: 300,
}
)
const renderConfigButtons = () => {
return mobileButtonsConfig?.map((button, index) => {
const className = getButtonClassName(button)
let label: string | JSX.Element = button.icon || button.label || ''
if (typeof label === 'string' && label.startsWith('pixelarticons:')) {
const iconName = label.replace('pixelarticons:', '')
label = <PixelartIcon iconName={iconName} />
}
const onPointerDown = (e: React.PointerEvent) => {
const elem = e.currentTarget as HTMLElement
elem.setPointerCapture(e.pointerId)
if (longPressTimerIdRef.current) {
clearTimeout(longPressTimerIdRef.current)
longPressTimerIdRef.current = null
}
actionToShortPressRef.current = null
const { actionHold, action } = button
if (actionHold) {
if (typeof actionHold === 'object' && 'command' in actionHold) {
const config = actionHold
if (config.longPressAction) {
actionToShortPressRef.current = config.command
longPressTimerIdRef.current = window.setTimeout(() => {
handleMobileButtonLongPress(config)
actionToShortPressRef.current = null
longPressTimerIdRef.current = null
}, config.duration || 500)
} else {
handleMobileButtonActionCommand(config.command, true)
}
} else if (action) {
actionToShortPressRef.current = action
longPressTimerIdRef.current = window.setTimeout(() => {
handleMobileButtonActionCommand(actionHold, true)
actionToShortPressRef.current = null
longPressTimerIdRef.current = null
}, 500)
} else {
handleMobileButtonActionCommand(actionHold, true)
}
} else if (action) {
handleMobileButtonActionCommand(action, true)
}
}
const onPointerUp = (e: React.PointerEvent) => {
const elem = e.currentTarget as HTMLElement
elem.releasePointerCapture(e.pointerId)
const { actionHold, action } = button
let wasShortPressHandled = false
if (longPressTimerIdRef.current) {
clearTimeout(longPressTimerIdRef.current)
longPressTimerIdRef.current = null
if (actionToShortPressRef.current) {
handleMobileButtonActionCommand(actionToShortPressRef.current, true)
handleMobileButtonActionCommand(actionToShortPressRef.current, false)
wasShortPressHandled = true
}
}
if (!wasShortPressHandled) {
if (actionHold) {
if (typeof actionHold === 'object' && 'command' in actionHold) {
const config = actionHold
if (config.longPressAction) {
if (actionToShortPressRef.current === null) {
if (typeof config.longPressAction === 'string') {
handleMobileButtonActionCommand(config.longPressAction, false)
}
}
} else {
handleMobileButtonActionCommand(config.command, false)
}
} else if (action) {
if (actionToShortPressRef.current === null) {
handleMobileButtonActionCommand(actionHold, false)
}
} else {
handleMobileButtonActionCommand(actionHold, false)
}
} else if (action) {
handleMobileButtonActionCommand(action, false)
}
}
actionToShortPressRef.current = null
}
return (
<div
key={index}
className={className}
onPointerDown={onPointerDown}
onPointerUp={onPointerUp}
onLostPointerCapture={onPointerUp}
>
{label}
</div>
)
})
}
// ios note: just don't use <button>
return <div ref={elRef} className={styles['mobile-top-btns']} id="mobile-top">
<div
className={styles['debug-btn']} onPointerDown={(e) => {
window.dispatchEvent(new MouseEvent('mousedown', { button: 1 }))
}}
>S
return (
<div ref={elRef} className={styles['mobile-top-btns']} id="mobile-top">
{mobileButtonsConfig && mobileButtonsConfig.length > 0 ? renderConfigButtons() : null}
</div>
<div
className={styles['debug-btn']} onPointerDown={(e) => {
document.dispatchEvent(new KeyboardEvent('keydown', { code: 'F3' }))
document.dispatchEvent(new KeyboardEvent('keyup', { code: 'F3' }))
}} {...longPressEvent}
>F3
</div>
<div
className={styles['chat-btn']}
{...chatLongPressEvent}
onPointerUp={(e) => {
document.dispatchEvent(new KeyboardEvent('keyup', { key: 'Tab' }))
}}
/>
<div
className={styles['pause-btn']} onPointerDown={(e) => {
e.stopPropagation()
showModal({ reactType: 'pause-screen' })
}}
/>
</div>
)
}

View file

@ -16,7 +16,7 @@ export const Primary: Story = {
{ username: 'Player 1', ping: 10, uuid: '1' },
{ username: 'Player 2', ping: 20, uuid: '2' },
{ username: 'Player 3', ping: 30, uuid: '3' },
]
] as any
],
clientId: '2',
tablistHeader: 'Header',

View file

@ -2,7 +2,7 @@ import MessageFormattedString from './MessageFormattedString'
import './PlayerListOverlay.css'
type PlayersLists = Array<Array<Pick<import('mineflayer').Player, 'uuid' | 'username' | 'ping'>>>
type PlayersLists = Array<Array<Pick<import('mineflayer').Player, 'uuid' | 'displayName' | 'username' | 'ping'>>>
type PlayerListOverlayProps = {
playersLists: PlayersLists,
@ -25,7 +25,7 @@ export default ({ playersLists, clientId, tablistHeader, tablistFooter, serverIP
<div key={index} className="player-list">
{list.map(player => (
<div key={player.uuid ?? player.username} className={`playerlist-entry${clientId === player.uuid ? ' active-player' : ''}`} id={`plist-player-${player.uuid}`}>
<MessageFormattedString message={player.username} />
<MessageFormattedString message={player.displayName ?? player.username} />
<div className="playerlist-ping">
<p className="playerlist-ping-value">{player.ping}</p>
<p className="playerlist-ping-label">ms</p>

View file

@ -1,4 +1,4 @@
import { useSnapshot } from 'valtio'
import { proxy, useSnapshot } from 'valtio'
import { useState, useEffect, useMemo } from 'react'
import { isGameActive } from '../globalState'
import PlayerListOverlay from './PlayerListOverlay'
@ -9,26 +9,17 @@ const MAX_ROWS_PER_COL = 10
type Players = typeof bot.players
export const tabListState = proxy({
isOpen: false,
})
export default () => {
const { isOpen } = useSnapshot(tabListState)
const serverIp = lastConnectOptions.value?.server
const [clientId, setClientId] = useState(bot._client.uuid)
const [players, setPlayers] = useState<Players>({})
const [isOpen, setIsOpen] = useState(false)
const handleKeyDown = (e) => {
if (!isGameActive(true)) return
if (e.key === 'Tab') {
setIsOpen(prev => true)
e.preventDefault()
}
}
const handleKeyUp = (e) => {
if (e.key === 'Tab') {
setIsOpen(prev => false)
e.preventDefault()
}
}
const [counter, setCounter] = useState(0)
useEffect(() => {
function requestUpdate () {
@ -57,12 +48,11 @@ export default () => {
})
}
document.addEventListener('keydown', handleKeyDown)
document.addEventListener('keyup', handleKeyUp)
const playerlistHeader = () => setCounter(prev => prev + 1)
bot._client.on('playerlist_header', playerlistHeader)
return () => {
document.removeEventListener('keydown', handleKeyDown)
document.removeEventListener('keyup', handleKeyUp)
bot?._client.removeListener('playerlist_header', playerlistHeader)
}
}, [serverIp])

View file

@ -287,7 +287,6 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
proxy: overrides.proxyOverride || getCurrentProxy(),
botVersion: overrides.versionOverride ?? /* legacy */ overrides['version'],
ignoreQs: true,
autoLoginPassword: server?.autoLogin?.[username],
authenticatedAccount,
saveServerToHistory: shouldSave,
onSuccessfulPlay () {

View file

@ -1,18 +1,23 @@
.hint_container {
position: fixed;
top: 20%;
left: 0;
right: 0;
bottom: calc(var(--safe-area-inset-bottom) + 55px);
margin: 0 auto;
width: fit-content;
display: flex;
align-items: center;
gap: 8px;
pointer-events: none;
pointer-events: auto;
z-index: 1000;
text-shadow: 1px 1px 8px rgba(0, 0, 0, 1);
}
.hint_container > button {
width: auto;
padding: 0 10px;
}
.hint_text {
color: white;
font-size: 10px;

View file

@ -3,28 +3,33 @@ import { useSnapshot } from 'valtio'
import { options } from '../optionsStorage'
import { activeModalStack } from '../globalState'
import { videoCursorInteraction } from '../customChannels'
import PixelartIcon, { pixelartIcons } from './PixelartIcon'
// import PixelartIcon, { pixelartIcons } from './PixelartIcon'
import styles from './TouchInteractionHint.module.css'
import { useUsingTouch } from './utilsApp'
import Button from './Button'
export default () => {
const usingTouch = useUsingTouch()
const modalStack = useSnapshot(activeModalStack)
const { touchInteractionType } = useSnapshot(options)
const [hintText, setHintText] = useState<string | null>(null)
const [entityName, setEntityName] = useState<string | null>(null)
useEffect(() => {
const update = () => {
const videoInteraction = videoCursorInteraction()
if (videoInteraction) {
setHintText(`Interact with video`)
setEntityName(null)
} else {
const cursorState = bot.mouse.getCursorState()
if (cursorState.entity) {
const entityName = cursorState.entity.displayName ?? cursorState.entity.name
setHintText(`Attack ${entityName}`)
const name = cursorState.entity.displayName ?? cursorState.entity.name ?? 'Entity'
setHintText(`Attack ${name}`)
setEntityName(name)
} else {
setHintText(null)
setEntityName(null)
}
}
}
@ -40,13 +45,33 @@ export default () => {
}
}, [])
const handleUseButtonClick = (e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
document.dispatchEvent(new MouseEvent('mousedown', { button: 2 }))
bot.mouse.update()
document.dispatchEvent(new MouseEvent('mouseup', { button: 2 }))
}
if (!usingTouch || touchInteractionType !== 'classic' || modalStack.length > 0) return null
if (!hintText) return null
if (!hintText && !entityName) return null
// need to hide "Use" button if there isn't an entity name, but there is a hint text
if (!entityName) return null
return (
<div className={`${styles.hint_container} interaction-hint`}>
<PixelartIcon iconName={pixelartIcons['sun-alt']} width={14} />
<span className={styles.hint_text}>{hintText}</span>
<div
className={`${styles.hint_container} interaction-hint`}
>
{/* temporary hide hint indicator and text */}
{/* <PixelartIcon iconName={pixelartIcons['sun-alt']} width={14} />
<span className={styles.hint_text}>{hintText || 'Attack entity'}</span> */}
<Button
onClick={handleUseButtonClick}
>
{`Use ${entityName}`}
</Button>
</div>
)
}

View file

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

View file

@ -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<NodeJS.Timeout | null>(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
}
}

View file

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

View file

@ -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 = () => {
<MineflayerPluginHud />
<MineflayerPluginConsole />
{showUI && <TouchInteractionHint />}
<GlobalOverlayHints />
<div style={{ display: showUI ? 'block' : 'none' }}>
{!disabledUiParts.includes('xp-bar') && <XPBarProvider />}
{!disabledUiParts.includes('hud-bars') && <HudBarsProvider />}
@ -241,7 +244,7 @@ const App = () => {
<ModsPage />
<SelectOption />
<CreditsAboutModal />
<NoModalFoundProvider />
</RobustPortal>
<RobustPortal to={document.body}>

View file

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

67
src/utils/splashText.ts Normal file
View file

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

View file

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