Release (#273)
This commit is contained in:
commit
d60f9fc55f
91 changed files with 7363 additions and 21569 deletions
|
|
@ -14,13 +14,14 @@ For building the project yourself / contributing, see [Development, Debugging &
|
|||
|
||||
- Open any zip world file or even folder in read-write mode!
|
||||
- Connect to Java servers running in both offline (cracked) and online mode* (it's possible because of proxy servers, see below)
|
||||
- Integrated JS server capable of opening Java world saves in any way (folders, zip, web streaming, etc)
|
||||
- Integrated JS server clone capable of opening Java world saves in any way (folders, zip, web chunks streaming, etc)
|
||||
- Singleplayer mode with simple world generations!
|
||||
- Works offline
|
||||
- First-class touch (mobile) & controller support
|
||||
- First-class keybindings configuration
|
||||
- Advanced Resource pack support: Custom GUI, all textures. Server resource packs are supported with proper CORS configuration.
|
||||
- Builtin JEI with recipes & descriptions for every item (JEI is creative inventory replacement)
|
||||
- Builtin JEI with recipes & descriptions for almost every item (JEI is creative inventory replacement)
|
||||
- Custom protocol channel extensions (eg for custom block models in the world)
|
||||
- Play with friends over internet! (P2P is powered by Peer.js discovery servers)
|
||||
- ~~Google Drive support for reading / saving worlds back to the cloud~~
|
||||
- even even more!
|
||||
|
|
@ -152,6 +153,7 @@ Server specific:
|
|||
- `?proxy=<proxy_address>` - Set the proxy server address to use for the server
|
||||
- `?username=<username>` - Set the username for the server
|
||||
- `?lockConnect=true` - Only works then `ip` parameter is set. Disables cancel/save buttons and all inputs in the connect screen already set as parameters. Useful for integrates iframes.
|
||||
- `?autoConnect=true` - Only works then `ip` and `version` parameters are set and `allowAutoConnect` is `true` in config.json! Directly connects to the specified server. Useful for integrates iframes.
|
||||
- `?serversList=<list_or_url>` - `<list_or_url>` can be a list of servers in the format `ip:version,ip` or a url to a json file with the same format (array) or a txt file with line-delimited list of server IPs.
|
||||
|
||||
Single player specific:
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@
|
|||
{
|
||||
"ip": "ws://play.mcraft.fun"
|
||||
},
|
||||
{
|
||||
"ip": "ws://play2.mcraft.fun"
|
||||
},
|
||||
{
|
||||
"ip": "kaboom.pw",
|
||||
"version": "1.20.3",
|
||||
|
|
|
|||
|
|
@ -54,8 +54,8 @@
|
|||
})
|
||||
})
|
||||
}
|
||||
window.lastError = errorOrMessage instanceof Error ? errorOrMessage : new Error(errorOrMessage)
|
||||
}
|
||||
window.lastError = errorOrMessage instanceof Error ? errorOrMessage : new Error(errorOrMessage)
|
||||
}
|
||||
window.addEventListener('unhandledrejection', (e) => onError(e.reason, true))
|
||||
window.addEventListener('error', (e) => onError(e.error ?? e.message))
|
||||
|
|
|
|||
28
package.json
28
package.json
|
|
@ -63,6 +63,7 @@
|
|||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"debug": "^4.3.4",
|
||||
"deepslate": "^0.23.5",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"eruda": "^3.0.1",
|
||||
"esbuild": "^0.19.3",
|
||||
|
|
@ -144,7 +145,7 @@
|
|||
"http-browserify": "^1.7.0",
|
||||
"http-server": "^14.1.1",
|
||||
"https-browserify": "^1.0.0",
|
||||
"mc-assets": "^0.2.31",
|
||||
"mc-assets": "^0.2.34",
|
||||
"minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next",
|
||||
"mineflayer": "github:zardoy/mineflayer",
|
||||
"mineflayer-pathfinder": "^2.4.4",
|
||||
|
|
@ -168,15 +169,22 @@
|
|||
"cypress-plugin-snapshots": "^1.4.4",
|
||||
"systeminformation": "^5.21.22"
|
||||
},
|
||||
"browserslist": [
|
||||
"iOS >= 14",
|
||||
"Android >= 13",
|
||||
"Chrome >= 103",
|
||||
"not dead",
|
||||
"not ie <= 11",
|
||||
"not op_mini all",
|
||||
"> 0.5%"
|
||||
],
|
||||
"browserslist": {
|
||||
"production": [
|
||||
"iOS >= 14",
|
||||
"Android >= 13",
|
||||
"Chrome >= 103",
|
||||
"not dead",
|
||||
"not ie <= 11",
|
||||
"not op_mini all",
|
||||
"> 0.5%"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"buffer": "^6.0.3",
|
||||
|
|
|
|||
72
pnpm-lock.yaml
generated
72
pnpm-lock.yaml
generated
|
|
@ -97,6 +97,9 @@ importers:
|
|||
debug:
|
||||
specifier: ^4.3.4
|
||||
version: 4.4.0(supports-color@8.1.1)
|
||||
deepslate:
|
||||
specifier: ^0.23.5
|
||||
version: 0.23.5
|
||||
diff-match-patch:
|
||||
specifier: ^1.0.5
|
||||
version: 1.0.5
|
||||
|
|
@ -132,7 +135,7 @@ importers:
|
|||
version: 4.17.21
|
||||
mcraft-fun-mineflayer:
|
||||
specifier: 0.0.3
|
||||
version: 0.0.3(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/54f8c2282d822ad02967a197bda36302a4e7b4a5(encoding@0.1.13))
|
||||
version: 0.0.3(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/4fa1af9964cab91315d8d1ae02615f3039638828(encoding@0.1.13))
|
||||
minecraft-data:
|
||||
specifier: 3.83.1
|
||||
version: 3.83.1
|
||||
|
|
@ -346,14 +349,14 @@ importers:
|
|||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
mc-assets:
|
||||
specifier: ^0.2.31
|
||||
version: 0.2.31
|
||||
specifier: ^0.2.34
|
||||
version: 0.2.34
|
||||
minecraft-inventory-gui:
|
||||
specifier: github:zardoy/minecraft-inventory-gui#next
|
||||
version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/75e940a4cd50d89e0ba03db3733d5d704917a3c8(@types/react@18.2.20)(react@18.2.0)
|
||||
mineflayer:
|
||||
specifier: github:zardoy/mineflayer
|
||||
version: https://codeload.github.com/zardoy/mineflayer/tar.gz/54f8c2282d822ad02967a197bda36302a4e7b4a5(encoding@0.1.13)
|
||||
version: https://codeload.github.com/zardoy/mineflayer/tar.gz/4fa1af9964cab91315d8d1ae02615f3039638828(encoding@0.1.13)
|
||||
mineflayer-pathfinder:
|
||||
specifier: ^2.4.4
|
||||
version: 2.4.4
|
||||
|
|
@ -3791,6 +3794,9 @@ packages:
|
|||
character-entities@2.0.2:
|
||||
resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
|
||||
|
||||
charenc@0.0.2:
|
||||
resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==}
|
||||
|
||||
check-error@1.0.3:
|
||||
resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==}
|
||||
|
||||
|
|
@ -4065,6 +4071,9 @@ packages:
|
|||
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
crypt@0.0.2:
|
||||
resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==}
|
||||
|
||||
crypto-browserify@3.12.0:
|
||||
resolution: {integrity: sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==}
|
||||
|
||||
|
|
@ -4218,6 +4227,9 @@ packages:
|
|||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
deepslate@0.23.5:
|
||||
resolution: {integrity: sha512-FjBBbuPUI1Y/dXtUc4WiCJSA7s7yRAXepD7qWRF6wX5m/q7AVRauMEShu8lphRvqCtJyxcYFZmISwX5OOH/tWw==}
|
||||
|
||||
default-browser-id@3.0.0:
|
||||
resolution: {integrity: sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==}
|
||||
engines: {node: '>=12'}
|
||||
|
|
@ -5183,6 +5195,9 @@ packages:
|
|||
github-slugger@1.5.0:
|
||||
resolution: {integrity: sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==}
|
||||
|
||||
gl-matrix@3.4.3:
|
||||
resolution: {integrity: sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==}
|
||||
|
||||
gl@6.0.2:
|
||||
resolution: {integrity: sha512-yBbfpChOtFvg5D+KtMaBFvj6yt3vUnheNAH+UrQH2TfDB8kr0tERdL0Tjhe0W7xJ6jR6ftQBluTZR9jXUnKe8g==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
|
@ -6228,8 +6243,8 @@ packages:
|
|||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
mc-assets@0.2.31:
|
||||
resolution: {integrity: sha512-+WAU9+iRbBgWWgsrQ52j0Xqq+Qs7UbjlVtZq1NCTqpIAvaEKNCwM7u2qFV5hoj4RpBAsVmkgLissdjLddE8WaA==}
|
||||
mc-assets@0.2.34:
|
||||
resolution: {integrity: sha512-BvE2mVs9XETLFb+FN1Zbc4mJ+CvZqgxVd3kxhERp1QljudDmMsWsMcK2EUTrevuE6a7L3F2kx8XC1vVA79i/ow==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
mcraft-fun-mineflayer@0.0.3:
|
||||
|
|
@ -6251,6 +6266,9 @@ packages:
|
|||
md5.js@1.3.5:
|
||||
resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==}
|
||||
|
||||
md5@2.3.0:
|
||||
resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==}
|
||||
|
||||
mdast-util-definitions@4.0.0:
|
||||
resolution: {integrity: sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ==}
|
||||
|
||||
|
|
@ -6462,8 +6480,8 @@ packages:
|
|||
resolution: {integrity: sha512-q7cmpZFaSI6sodcMJxc2GkV8IO84HbsUP+xNipGKfGg+FMISKabzdJ838Axb60qRtZrp6ny7LluQE7lesHvvxQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/54f8c2282d822ad02967a197bda36302a4e7b4a5:
|
||||
resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/54f8c2282d822ad02967a197bda36302a4e7b4a5}
|
||||
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/4fa1af9964cab91315d8d1ae02615f3039638828:
|
||||
resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/4fa1af9964cab91315d8d1ae02615f3039638828}
|
||||
version: 4.25.0
|
||||
engines: {node: '>=18'}
|
||||
|
||||
|
|
@ -6874,6 +6892,9 @@ packages:
|
|||
pako@1.0.11:
|
||||
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||
|
||||
pako@2.1.0:
|
||||
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
|
||||
|
||||
param-case@3.0.4:
|
||||
resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
|
||||
|
||||
|
|
@ -9258,6 +9279,9 @@ packages:
|
|||
resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==}
|
||||
engines: {node: '>=12.20'}
|
||||
|
||||
zod@3.24.1:
|
||||
resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==}
|
||||
|
||||
zustand@3.6.5:
|
||||
resolution: {integrity: sha512-/WfLJuXiEJimt61KGMHebrFBwckkCHGhAgVXTgPQHl6IMzjqm6MREb1OnDSnCRiSmRdhgdFCctceg6tSm79hiw==}
|
||||
engines: {node: '>=12.7.0'}
|
||||
|
|
@ -13604,6 +13628,8 @@ snapshots:
|
|||
|
||||
character-entities@2.0.2: {}
|
||||
|
||||
charenc@0.0.2: {}
|
||||
|
||||
check-error@1.0.3:
|
||||
dependencies:
|
||||
get-func-name: 2.0.2
|
||||
|
|
@ -13924,6 +13950,8 @@ snapshots:
|
|||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
crypt@0.0.2: {}
|
||||
|
||||
crypto-browserify@3.12.0:
|
||||
dependencies:
|
||||
browserify-cipher: 1.0.1
|
||||
|
|
@ -14125,6 +14153,12 @@ snapshots:
|
|||
|
||||
deepmerge@4.3.1: {}
|
||||
|
||||
deepslate@0.23.5:
|
||||
dependencies:
|
||||
gl-matrix: 3.4.3
|
||||
md5: 2.3.0
|
||||
pako: 2.1.0
|
||||
|
||||
default-browser-id@3.0.0:
|
||||
dependencies:
|
||||
bplist-parser: 0.2.0
|
||||
|
|
@ -15462,6 +15496,8 @@ snapshots:
|
|||
|
||||
github-slugger@1.5.0: {}
|
||||
|
||||
gl-matrix@3.4.3: {}
|
||||
|
||||
gl@6.0.2:
|
||||
dependencies:
|
||||
bindings: 1.5.0
|
||||
|
|
@ -16643,14 +16679,16 @@ snapshots:
|
|||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
mc-assets@0.2.31: {}
|
||||
mc-assets@0.2.34:
|
||||
dependencies:
|
||||
zod: 3.24.1
|
||||
|
||||
mcraft-fun-mineflayer@0.0.3(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/54f8c2282d822ad02967a197bda36302a4e7b4a5(encoding@0.1.13)):
|
||||
mcraft-fun-mineflayer@0.0.3(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/4fa1af9964cab91315d8d1ae02615f3039638828(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/e9eb551ba30ec2e742c49e6927be6402b413bb76(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13)
|
||||
mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/54f8c2282d822ad02967a197bda36302a4e7b4a5(encoding@0.1.13)
|
||||
mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/4fa1af9964cab91315d8d1ae02615f3039638828(encoding@0.1.13)
|
||||
prismarine-item: 1.16.0
|
||||
ws: 8.18.0
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -16667,6 +16705,12 @@ snapshots:
|
|||
inherits: 2.0.4
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
md5@2.3.0:
|
||||
dependencies:
|
||||
charenc: 0.0.2
|
||||
crypt: 0.0.2
|
||||
is-buffer: 1.1.6
|
||||
|
||||
mdast-util-definitions@4.0.0:
|
||||
dependencies:
|
||||
unist-util-visit: 2.0.3
|
||||
|
|
@ -17042,7 +17086,7 @@ snapshots:
|
|||
- encoding
|
||||
- supports-color
|
||||
|
||||
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/54f8c2282d822ad02967a197bda36302a4e7b4a5(encoding@0.1.13):
|
||||
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/4fa1af9964cab91315d8d1ae02615f3039638828(encoding@0.1.13):
|
||||
dependencies:
|
||||
minecraft-data: 3.83.1
|
||||
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/e9eb551ba30ec2e742c49e6927be6402b413bb76(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13)
|
||||
|
|
@ -17556,6 +17600,8 @@ snapshots:
|
|||
|
||||
pako@1.0.11: {}
|
||||
|
||||
pako@2.1.0: {}
|
||||
|
||||
param-case@3.0.4:
|
||||
dependencies:
|
||||
dot-case: 3.0.4
|
||||
|
|
@ -20407,6 +20453,8 @@ snapshots:
|
|||
|
||||
yocto-queue@1.0.0: {}
|
||||
|
||||
zod@3.24.1: {}
|
||||
|
||||
zustand@3.6.5(react@18.2.0):
|
||||
optionalDependencies:
|
||||
react: 18.2.0
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ const buildOptions = {
|
|||
'debugger'
|
||||
] : [],
|
||||
sourcemap: 'linked',
|
||||
target: watch ? undefined : ['ios14'],
|
||||
write: false,
|
||||
metafile: true,
|
||||
outdir: path.join(__dirname, './dist'),
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import * as scenes from './scenes'
|
|||
|
||||
const qsScene = new URLSearchParams(window.location.search).get('scene')
|
||||
const Scene: typeof BasePlaygroundScene = qsScene ? scenes[qsScene] : scenes.main
|
||||
playgroundGlobalUiState.scenes = ['main', 'railsCobweb', 'floorRandom', 'lightingStarfield', 'transparencyIssue', 'entities', 'frequentUpdates', 'slabsOptimization']
|
||||
playgroundGlobalUiState.scenes = ['main', 'railsCobweb', 'floorRandom', 'lightingStarfield', 'transparencyIssue', 'entities', 'frequentUpdates', 'slabsOptimization', 'allEntities']
|
||||
playgroundGlobalUiState.selected = qsScene ?? 'main'
|
||||
|
||||
const scene = new Scene()
|
||||
|
|
|
|||
165
renderer/playground/scenes/allEntities.ts
Normal file
165
renderer/playground/scenes/allEntities.ts
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
import { BasePlaygroundScene } from '../baseScene'
|
||||
import { EntityDebugFlags, EntityMesh, rendererSpecialHandled } from '../../viewer/lib/entity/EntityMesh'
|
||||
|
||||
export default class AllEntities extends BasePlaygroundScene {
|
||||
continuousRender = false
|
||||
enableCameraControls = false
|
||||
|
||||
async initData () {
|
||||
await super.initData()
|
||||
|
||||
// Create results container
|
||||
const container = document.createElement('div')
|
||||
container.style.cssText = `
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
background: rgba(0,0,0,0.8);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
font-family: monospace;
|
||||
min-width: 400px;
|
||||
`
|
||||
document.body.appendChild(container)
|
||||
|
||||
// Add title
|
||||
const title = document.createElement('h2')
|
||||
title.textContent = 'Minecraft Entity Support'
|
||||
title.style.cssText = 'margin-top: 0; text-align: center;'
|
||||
container.appendChild(title)
|
||||
|
||||
// Test entities
|
||||
const results: Array<{
|
||||
entity: string;
|
||||
supported: boolean;
|
||||
type?: 'obj' | 'bedrock';
|
||||
mappedFrom?: string;
|
||||
textureMap?: boolean;
|
||||
errors?: string[];
|
||||
}> = []
|
||||
const { mcData } = window
|
||||
const entityNames = Object.keys(mcData.entitiesArray.reduce((acc, entity) => {
|
||||
acc[entity.name] = true
|
||||
return acc
|
||||
}, {}))
|
||||
|
||||
// Add loading indicator
|
||||
const loading = document.createElement('div')
|
||||
loading.textContent = 'Testing entities...'
|
||||
loading.style.textAlign = 'center'
|
||||
container.appendChild(loading)
|
||||
|
||||
for (const entity of entityNames) {
|
||||
const debugFlags: EntityDebugFlags = {}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new EntityMesh(this.version, entity, viewer.world, {}, debugFlags)
|
||||
|
||||
results.push({
|
||||
entity,
|
||||
supported: !!debugFlags.type || rendererSpecialHandled.includes(entity),
|
||||
type: debugFlags.type,
|
||||
mappedFrom: debugFlags.tempMap,
|
||||
textureMap: debugFlags.textureMap,
|
||||
errors: debugFlags.errors
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
results.push({
|
||||
entity,
|
||||
supported: false,
|
||||
mappedFrom: debugFlags.tempMap
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Remove loading indicator
|
||||
loading.remove()
|
||||
|
||||
const createSection = (title: string, items: any[], filter: (item: any) => boolean) => {
|
||||
const section = document.createElement('div')
|
||||
section.style.marginBottom = '20px'
|
||||
|
||||
const sectionTitle = document.createElement('h3')
|
||||
sectionTitle.textContent = title
|
||||
sectionTitle.style.textAlign = 'center'
|
||||
section.appendChild(sectionTitle)
|
||||
|
||||
const list = document.createElement('ul')
|
||||
list.style.cssText = 'padding-left: 20px; list-style-type: none; margin: 0;'
|
||||
|
||||
const filteredItems = items.filter(filter)
|
||||
for (const item of filteredItems) {
|
||||
const listItem = document.createElement('li')
|
||||
listItem.style.cssText = 'line-height: 1.4; margin: 8px 0;'
|
||||
|
||||
const entityName = document.createElement('strong')
|
||||
entityName.style.cssText = 'user-select: text;-webkit-user-select: text;'
|
||||
entityName.textContent = item.entity
|
||||
listItem.appendChild(entityName)
|
||||
|
||||
let text = ''
|
||||
if (item.mappedFrom) {
|
||||
text += ` -> ${item.mappedFrom}`
|
||||
}
|
||||
if (item.type) {
|
||||
text += ` - ${item.type}`
|
||||
}
|
||||
if (item.textureMap) {
|
||||
text += ' ⚠️'
|
||||
}
|
||||
if (item.errors) {
|
||||
text += ' ❌'
|
||||
}
|
||||
|
||||
listItem.appendChild(document.createTextNode(text))
|
||||
list.appendChild(listItem)
|
||||
}
|
||||
|
||||
section.appendChild(list)
|
||||
return { section, count: filteredItems.length }
|
||||
}
|
||||
|
||||
// Sort results - bedrock first
|
||||
results.sort((a, b) => {
|
||||
if (a.type === 'bedrock' && b.type !== 'bedrock') return -1
|
||||
if (a.type !== 'bedrock' && b.type === 'bedrock') return 1
|
||||
return a.entity.localeCompare(b.entity)
|
||||
})
|
||||
|
||||
// Add sections
|
||||
const sections = [
|
||||
{
|
||||
title: '❌ Unsupported Entities',
|
||||
filter: (r: any) => !r.supported && !r.mappedFrom
|
||||
},
|
||||
{
|
||||
title: '⚠️ Partially Supported Entities',
|
||||
filter: (r: any) => r.mappedFrom
|
||||
},
|
||||
{
|
||||
title: '✅ Supported Entities',
|
||||
filter: (r: any) => r.supported && !r.mappedFrom
|
||||
}
|
||||
]
|
||||
|
||||
for (const { title, filter } of sections) {
|
||||
const { section, count } = createSection(title, results, filter)
|
||||
if (count > 0) {
|
||||
container.appendChild(section)
|
||||
}
|
||||
}
|
||||
|
||||
// log object with errors per entity
|
||||
const errors = results.filter(r => r.errors).map(r => ({
|
||||
entity: r.entity,
|
||||
errors: r.errors
|
||||
}))
|
||||
console.log(errors)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,3 +8,4 @@ export { default as rotationIssue } from './rotationIssue'
|
|||
export { default as entities } from './entities'
|
||||
export { default as frequentUpdates } from './frequentUpdates'
|
||||
export { default as slabsOptimization } from './slabsOptimization'
|
||||
export { default as allEntities } from './allEntities'
|
||||
|
|
|
|||
|
|
@ -295,7 +295,7 @@ class MainScene extends BasePlaygroundScene {
|
|||
}
|
||||
}
|
||||
|
||||
worldView!.setBlockStateId(this.targetPos, block.stateId)
|
||||
worldView!.setBlockStateId(this.targetPos, block.stateId ?? 0)
|
||||
console.log('up stateId', block.stateId)
|
||||
this.params.metadata = block.metadata
|
||||
this.metadataGui.updateDisplay()
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
import { defineConfig, mergeRsbuildConfig } from '@rsbuild/core';
|
||||
import { defineConfig, mergeRsbuildConfig, RsbuildPluginAPI } from '@rsbuild/core';
|
||||
import supportedVersions from '../src/supportedVersions.mjs'
|
||||
import childProcess from 'child_process'
|
||||
import path, { dirname, join } from 'path'
|
||||
import { pluginReact } from '@rsbuild/plugin-react';
|
||||
import { pluginNodePolyfill } from '@rsbuild/plugin-node-polyfill';
|
||||
import fs from 'fs'
|
||||
import fsExtra from 'fs-extra'
|
||||
import { appAndRendererSharedConfig, rspackViewerConfig } from './rsbuildSharedConfig';
|
||||
|
||||
const mcDataPath = join(__dirname, '../generated/minecraft-data-optimized.json')
|
||||
|
||||
// if (!fs.existsSync('./playground/textures')) {
|
||||
// fsExtra.copySync('node_modules/mc-assets/dist/other-textures/latest/entity', './playground/textures/entity')
|
||||
// }
|
||||
|
||||
if (!fs.existsSync(mcDataPath)) {
|
||||
childProcess.execSync('tsx ./scripts/makeOptimizedMcData.mjs', { stdio: 'inherit', cwd: path.join(__dirname, '..') })
|
||||
}
|
||||
|
|
@ -36,5 +41,19 @@ export default mergeRsbuildConfig(
|
|||
'globalThis.includedVersions': JSON.stringify(supportedVersions),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
name: 'test',
|
||||
setup (build: RsbuildPluginAPI) {
|
||||
const prep = async () => {
|
||||
fsExtra.copySync(join(__dirname, '../node_modules/mc-assets/dist/other-textures/latest/entity'), join(__dirname, './dist/textures/entity'))
|
||||
}
|
||||
build.onBeforeBuild(async () => {
|
||||
await prep()
|
||||
})
|
||||
build.onBeforeStartDevServer(() => prep())
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
|
|
|
|||
174
renderer/viewer/lib/DebugGui.ts
Normal file
174
renderer/viewer/lib/DebugGui.ts
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
// eslint-disable-next-line import/no-named-as-default
|
||||
import GUI from 'lil-gui'
|
||||
|
||||
export interface ParamMeta {
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
}
|
||||
|
||||
export class DebugGui {
|
||||
private gui: GUI
|
||||
private readonly storageKey: string
|
||||
private target: any
|
||||
private readonly params: string[]
|
||||
private readonly paramsMeta: Record<string, ParamMeta>
|
||||
private _visible = false // Default to not visible
|
||||
private readonly initialValues: Record<string, any> = {} // Store initial values
|
||||
private initialized = false
|
||||
|
||||
constructor (id: string, target: any, params?: string[], paramsMeta?: Record<string, ParamMeta>) {
|
||||
this.gui = new GUI()
|
||||
this.storageKey = `debug_params_${id}`
|
||||
this.target = target
|
||||
this.paramsMeta = paramsMeta ?? {}
|
||||
this.params = params ?? Object.keys(target)
|
||||
|
||||
// Store initial values
|
||||
for (const param of this.params) {
|
||||
this.initialValues[param] = target[param]
|
||||
}
|
||||
|
||||
// Hide by default
|
||||
this.gui.domElement.style.display = 'none'
|
||||
}
|
||||
|
||||
// Initialize and show the GUI
|
||||
activate () {
|
||||
if (!this.initialized) {
|
||||
this.loadSavedValues()
|
||||
this.setupControls()
|
||||
this.initialized = true
|
||||
}
|
||||
this.show()
|
||||
return this
|
||||
}
|
||||
|
||||
// Getter for visibility
|
||||
get visible (): boolean {
|
||||
return this._visible
|
||||
}
|
||||
|
||||
// Setter for visibility
|
||||
set visible (value: boolean) {
|
||||
this._visible = value
|
||||
this.gui.domElement.style.display = value ? 'block' : 'none'
|
||||
this.saveVisibility()
|
||||
}
|
||||
|
||||
private loadSavedValues () {
|
||||
try {
|
||||
const saved = localStorage.getItem(this.storageKey)
|
||||
if (saved) {
|
||||
const values = JSON.parse(saved)
|
||||
// Apply saved values to target
|
||||
for (const param of this.params) {
|
||||
if (param in values) {
|
||||
const value = values[param]
|
||||
if (value !== null) {
|
||||
this.target[param] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load debug values:', e)
|
||||
}
|
||||
}
|
||||
|
||||
private saveValues (deleteKey = false) {
|
||||
try {
|
||||
const values = {}
|
||||
for (const param of this.params) {
|
||||
values[param] = this.target[param]
|
||||
}
|
||||
if (deleteKey) {
|
||||
localStorage.removeItem(this.storageKey)
|
||||
} else {
|
||||
localStorage.setItem(this.storageKey, JSON.stringify(values))
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to save debug values:', e)
|
||||
}
|
||||
}
|
||||
|
||||
private saveVisibility () {
|
||||
try {
|
||||
localStorage.setItem(`${this.storageKey}_visible`, this._visible.toString())
|
||||
} catch (e) {
|
||||
console.warn('Failed to save debug visibility:', e)
|
||||
}
|
||||
}
|
||||
|
||||
private setupControls () {
|
||||
// Add visibility toggle at the top
|
||||
this.gui.add(this, 'visible').name('Show Controls')
|
||||
this.gui.add({ resetAll: () => {
|
||||
for (const param of this.params) {
|
||||
this.target[param] = this.initialValues[param]
|
||||
}
|
||||
this.saveValues(true)
|
||||
this.gui.destroy()
|
||||
this.gui = new GUI()
|
||||
this.setupControls()
|
||||
} }, 'resetAll').name('Reset All Parameters')
|
||||
|
||||
for (const param of this.params) {
|
||||
const value = this.target[param]
|
||||
const meta = this.paramsMeta[param] ?? {}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
// For numbers, use meta values or calculate reasonable defaults
|
||||
const min = meta.min ?? value - Math.abs(value * 2)
|
||||
const max = meta.max ?? value + Math.abs(value * 2)
|
||||
const step = meta.step ?? Math.abs(value) / 100
|
||||
|
||||
this.gui.add(this.target, param, min, max, step)
|
||||
.onChange(() => this.saveValues())
|
||||
} else if (typeof value === 'boolean') {
|
||||
// For booleans, create a checkbox
|
||||
this.gui.add(this.target, param)
|
||||
.onChange(() => this.saveValues())
|
||||
} else if (typeof value === 'string' && ['x', 'y', 'z'].includes(param)) {
|
||||
// Special case for xyz coordinates
|
||||
const min = meta.min ?? -10
|
||||
const max = meta.max ?? 10
|
||||
const step = meta.step ?? 0.1
|
||||
|
||||
this.gui.add(this.target, param, min, max, step)
|
||||
.onChange(() => this.saveValues())
|
||||
} else if (Array.isArray(value)) {
|
||||
// For arrays, create a dropdown
|
||||
this.gui.add(this.target, param, value)
|
||||
.onChange(() => this.saveValues())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Method to manually trigger save
|
||||
save () {
|
||||
this.saveValues()
|
||||
this.saveVisibility()
|
||||
}
|
||||
|
||||
// Method to destroy the GUI and clean up
|
||||
destroy () {
|
||||
this.saveVisibility()
|
||||
this.gui.destroy()
|
||||
}
|
||||
|
||||
// Toggle visibility
|
||||
toggle () {
|
||||
this.visible = !this.visible
|
||||
}
|
||||
|
||||
// Show the GUI
|
||||
show () {
|
||||
this.visible = true
|
||||
}
|
||||
|
||||
// Hide the GUI
|
||||
hide () {
|
||||
this.visible = false
|
||||
}
|
||||
}
|
||||
75
renderer/viewer/lib/animationController.ts
Normal file
75
renderer/viewer/lib/animationController.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import * as tweenJs from '@tweenjs/tween.js'
|
||||
|
||||
export class AnimationController {
|
||||
private currentAnimation: tweenJs.Group | null = null
|
||||
private isAnimating = false
|
||||
private cancelRequested = false
|
||||
private completionCallbacks: Array<() => void> = []
|
||||
|
||||
/** Main method */
|
||||
async startAnimation (createAnimation: () => tweenJs.Group): Promise<void> {
|
||||
if (this.isAnimating) {
|
||||
await this.cancelCurrentAnimation()
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.isAnimating = true
|
||||
this.cancelRequested = false
|
||||
this.currentAnimation = createAnimation()
|
||||
|
||||
this.completionCallbacks.push(() => {
|
||||
this.isAnimating = false
|
||||
this.currentAnimation = null
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/** Main method */
|
||||
async cancelCurrentAnimation (): Promise<void> {
|
||||
if (!this.isAnimating) return
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.cancelRequested = true
|
||||
this.completionCallbacks.push(() => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
animationCycleFinish () {
|
||||
if (this.cancelRequested) this.forceFinish()
|
||||
}
|
||||
|
||||
forceFinish () {
|
||||
if (!this.isAnimating) return
|
||||
|
||||
if (this.currentAnimation) {
|
||||
for (const tween of this.currentAnimation.getAll()) tween.stop()
|
||||
this.currentAnimation.removeAll()
|
||||
this.currentAnimation = null
|
||||
}
|
||||
|
||||
this.isAnimating = false
|
||||
this.cancelRequested = false
|
||||
|
||||
const callbacks = [...this.completionCallbacks]
|
||||
this.completionCallbacks = []
|
||||
for (const cb of callbacks) cb()
|
||||
}
|
||||
|
||||
/** Required method */
|
||||
update () {
|
||||
if (this.currentAnimation) {
|
||||
this.currentAnimation.update()
|
||||
}
|
||||
}
|
||||
|
||||
get isActive () {
|
||||
return this.isAnimating
|
||||
}
|
||||
|
||||
get shouldCancel () {
|
||||
return this.cancelRequested
|
||||
}
|
||||
}
|
||||
88
renderer/viewer/lib/basePlayerState.ts
Normal file
88
renderer/viewer/lib/basePlayerState.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { EventEmitter } from 'events'
|
||||
import { Vec3 } from 'vec3'
|
||||
import TypedEmitter from 'typed-emitter'
|
||||
import { ItemSelector } from 'mc-assets/dist/itemDefinitions'
|
||||
import { proxy } from 'valtio'
|
||||
import { HandItemBlock } from './holdingBlock'
|
||||
|
||||
export type MovementState = 'NOT_MOVING' | 'WALKING' | 'SPRINTING' | 'SNEAKING'
|
||||
export type ItemSpecificContextProperties = Partial<Pick<ItemSelector['properties'], 'minecraft:using_item' | 'minecraft:use_duration' | 'minecraft:use_cycle' | 'minecraft:display_context'>>
|
||||
|
||||
|
||||
export type PlayerStateEvents = {
|
||||
heldItemChanged: (item: HandItemBlock | undefined, isLeftHand: boolean) => void
|
||||
}
|
||||
|
||||
export interface IPlayerState {
|
||||
getEyeHeight(): number
|
||||
getMovementState(): MovementState
|
||||
getVelocity(): Vec3
|
||||
isOnGround(): boolean
|
||||
isSneaking(): boolean
|
||||
isFlying(): boolean
|
||||
isSprinting (): boolean
|
||||
getItemUsageTicks?(): number
|
||||
// isUsingItem?(): boolean
|
||||
getHeldItem?(isLeftHand: boolean): HandItemBlock | undefined
|
||||
username?: string
|
||||
onlineMode?: boolean
|
||||
|
||||
events: TypedEmitter<PlayerStateEvents>
|
||||
|
||||
reactive: {
|
||||
playerSkin: string | undefined
|
||||
}
|
||||
}
|
||||
|
||||
export class BasePlayerState implements IPlayerState {
|
||||
reactive = proxy({
|
||||
playerSkin: undefined
|
||||
})
|
||||
protected movementState: MovementState = 'NOT_MOVING'
|
||||
protected velocity = new Vec3(0, 0, 0)
|
||||
protected onGround = true
|
||||
protected sneaking = false
|
||||
protected flying = false
|
||||
protected sprinting = false
|
||||
readonly events = new EventEmitter() as TypedEmitter<PlayerStateEvents>
|
||||
|
||||
getEyeHeight (): number {
|
||||
return 1.62
|
||||
}
|
||||
|
||||
getMovementState (): MovementState {
|
||||
return this.movementState
|
||||
}
|
||||
|
||||
getVelocity (): Vec3 {
|
||||
return this.velocity
|
||||
}
|
||||
|
||||
isOnGround (): boolean {
|
||||
return this.onGround
|
||||
}
|
||||
|
||||
isSneaking (): boolean {
|
||||
return this.sneaking
|
||||
}
|
||||
|
||||
isFlying (): boolean {
|
||||
return this.flying
|
||||
}
|
||||
|
||||
isSprinting (): boolean {
|
||||
return this.sprinting
|
||||
}
|
||||
|
||||
// For testing purposes
|
||||
setState (state: Partial<{
|
||||
movementState: MovementState
|
||||
velocity: Vec3
|
||||
onGround: boolean
|
||||
sneaking: boolean
|
||||
flying: boolean
|
||||
sprinting: boolean
|
||||
}>) {
|
||||
Object.assign(this, state)
|
||||
}
|
||||
}
|
||||
94
renderer/viewer/lib/cameraBobbing.ts
Normal file
94
renderer/viewer/lib/cameraBobbing.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
export class CameraBobbing {
|
||||
private walkDistance = 0
|
||||
private prevWalkDistance = 0
|
||||
private bobAmount = 0
|
||||
private prevBobAmount = 0
|
||||
private readonly gameTimer = new GameTimer()
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
constructor (
|
||||
private readonly BOB_FREQUENCY: number = Math.PI, // How fast the bob cycles
|
||||
private readonly BOB_BASE_AMPLITUDE: number = 0.5, // Base amplitude of the bob
|
||||
private readonly VERTICAL_MULTIPLIER: number = 1, // Vertical movement multiplier
|
||||
private readonly ROTATION_MULTIPLIER_Z: number = 3, // Roll rotation multiplier
|
||||
private readonly ROTATION_MULTIPLIER_X: number = 5 // Pitch rotation multiplier
|
||||
) {}
|
||||
|
||||
// Call this when player is moving
|
||||
public updateWalkDistance (distance: number): void {
|
||||
this.prevWalkDistance = this.walkDistance
|
||||
this.walkDistance = distance
|
||||
}
|
||||
|
||||
// Call this when player is moving to update bob amount
|
||||
public updateBobAmount (isMoving: boolean): void {
|
||||
const targetBob = isMoving ? 1 : 0
|
||||
this.prevBobAmount = this.bobAmount
|
||||
|
||||
// Update timing
|
||||
const ticks = this.gameTimer.update()
|
||||
const deltaTime = ticks / 20 // Convert ticks to seconds assuming 20 TPS
|
||||
|
||||
// Smooth transition for bob amount
|
||||
const bobDelta = (targetBob - this.bobAmount) * Math.min(1, deltaTime * 10)
|
||||
this.bobAmount += bobDelta
|
||||
}
|
||||
|
||||
// Call this in your render/animation loop
|
||||
public getBobbing (): { position: { x: number, y: number }, rotation: { x: number, z: number } } {
|
||||
// Interpolate walk distance
|
||||
const walkDist = this.prevWalkDistance +
|
||||
(this.walkDistance - this.prevWalkDistance) * this.gameTimer.partialTick
|
||||
|
||||
// Interpolate bob amount
|
||||
const bob = this.prevBobAmount +
|
||||
(this.bobAmount - this.prevBobAmount) * this.gameTimer.partialTick
|
||||
|
||||
// Calculate total distance for bob cycle
|
||||
const totalDist = -(walkDist * this.BOB_FREQUENCY)
|
||||
|
||||
// Calculate offsets
|
||||
const xOffset = Math.sin(totalDist) * bob * this.BOB_BASE_AMPLITUDE
|
||||
const yOffset = -Math.abs(Math.cos(totalDist) * bob) * this.VERTICAL_MULTIPLIER
|
||||
|
||||
// Calculate rotations (in radians)
|
||||
const zRot = (Math.sin(totalDist) * bob * this.ROTATION_MULTIPLIER_Z) * (Math.PI / 180)
|
||||
const xRot = (Math.abs(Math.cos(totalDist - 0.2) * bob) * this.ROTATION_MULTIPLIER_X) * (Math.PI / 180)
|
||||
|
||||
return {
|
||||
position: { x: xOffset, y: yOffset },
|
||||
rotation: { x: xRot, z: zRot }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GameTimer {
|
||||
private readonly msPerTick: number
|
||||
private lastMs: number
|
||||
public partialTick = 0
|
||||
|
||||
constructor (tickRate = 20) {
|
||||
this.msPerTick = 1000 / tickRate
|
||||
this.lastMs = performance.now()
|
||||
}
|
||||
|
||||
update (): number {
|
||||
const currentMs = performance.now()
|
||||
const deltaSinceLastTick = currentMs - this.lastMs
|
||||
|
||||
// Calculate how much of a tick has passed
|
||||
const tickDelta = deltaSinceLastTick / this.msPerTick
|
||||
this.lastMs = currentMs
|
||||
|
||||
// Add to accumulated partial ticks
|
||||
this.partialTick += tickDelta
|
||||
|
||||
// Get whole number of ticks that should occur
|
||||
const wholeTicks = Math.floor(this.partialTick)
|
||||
|
||||
// Keep the remainder as the new partial tick
|
||||
this.partialTick -= wholeTicks
|
||||
|
||||
return wholeTicks
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ import * as THREE from 'three'
|
|||
import { PlayerObject, PlayerAnimation } from 'skinview3d'
|
||||
import { loadSkinToCanvas, loadEarsToCanvasFromSkin, inferModelType, loadCapeToCanvas, loadImage } from 'skinview-utils'
|
||||
// todo replace with url
|
||||
import stevePng from 'mc-assets/dist/other-textures/latest/entity/player/wide/steve.png'
|
||||
import { degreesToRadians } from '@nxg-org/mineflayer-tracker/lib/mathUtils'
|
||||
import { NameTagObject } from 'skinview3d/libs/nametag'
|
||||
import { flat, fromFormattedString } from '@xmcl/text-component'
|
||||
|
|
@ -20,10 +19,12 @@ import * as Entity from './entity/EntityMesh'
|
|||
import { getMesh } from './entity/EntityMesh'
|
||||
import { WalkingGeneralSwing } from './entity/animations'
|
||||
import { disposeObject } from './threeJsUtils'
|
||||
import { armorModels } from './entity/objModels'
|
||||
import { armorModel, armorTextures } from './entity/armorModels'
|
||||
import { Viewer } from './viewer'
|
||||
import { getBlockMeshFromModel } from './holdingBlock'
|
||||
const { loadTexture } = globalThis.isElectron ? require('./utils.electron.js') : require('./utils')
|
||||
import { ItemSpecificContextProperties } from './basePlayerState'
|
||||
import { loadSkinImage, getLookupUrl, stevePngUrl, steveTexture } from './utils/skins'
|
||||
import { loadTexture } from './utils'
|
||||
|
||||
export const TWEEN_DURATION = 120
|
||||
|
||||
|
|
@ -217,7 +218,7 @@ export class Entities extends EventEmitter {
|
|||
itemsTexture: THREE.Texture | null = null
|
||||
cachedMapsImages = {} as Record<number, string>
|
||||
itemFrameMaps = {} as Record<number, Array<THREE.Mesh<THREE.PlaneGeometry, THREE.MeshLambertMaterial>>>
|
||||
getItemUv: undefined | ((item: Record<string, any>) => {
|
||||
getItemUv: undefined | ((item: Record<string, any>, specificProps: ItemSpecificContextProperties) => {
|
||||
texture: THREE.Texture;
|
||||
u: number;
|
||||
v: number;
|
||||
|
|
@ -229,6 +230,20 @@ export class Entities extends EventEmitter {
|
|||
modelName: string
|
||||
})
|
||||
|
||||
get entitiesByName (): Record<string, SceneEntity[]> {
|
||||
const byName: Record<string, SceneEntity[]> = {}
|
||||
for (const entity of Object.values(this.entities)) {
|
||||
if (!entity['realName']) continue
|
||||
byName[entity['realName']] = byName[entity['realName']] || []
|
||||
byName[entity['realName']].push(entity)
|
||||
}
|
||||
return byName
|
||||
}
|
||||
|
||||
get entitiesRenderingCount (): number {
|
||||
return Object.values(this.entities).filter(entity => entity.visible).length
|
||||
}
|
||||
|
||||
constructor (public viewer: Viewer) {
|
||||
super()
|
||||
this.entitiesOptions = {}
|
||||
|
|
@ -289,12 +304,12 @@ export class Entities extends EventEmitter {
|
|||
const distanceSquared = dx * dx + dy * dy + dz * dz
|
||||
|
||||
// Get chunk coordinates
|
||||
const chunkX = Math.floor(entity.position.x / 16)
|
||||
const chunkZ = Math.floor(entity.position.z / 16)
|
||||
const chunkX = Math.floor(entity.position.x / 16) * 16
|
||||
const chunkZ = Math.floor(entity.position.z / 16) * 16
|
||||
const chunkKey = `${chunkX},${chunkZ}`
|
||||
|
||||
// Entity is visible if within 16 blocks OR in a finished chunk
|
||||
entity.visible = distanceSquared < VISIBLE_DISTANCE || this.viewer.world.finishedChunks[chunkKey]
|
||||
entity.visible = !!(distanceSquared < VISIBLE_DISTANCE || this.viewer.world.finishedChunks[chunkKey])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -304,12 +319,15 @@ export class Entities extends EventEmitter {
|
|||
return playerObject
|
||||
}
|
||||
|
||||
// fixme workaround
|
||||
defaultSteveTexture
|
||||
|
||||
uuidPerSkinUrlsCache = {} as Record<string, { skinUrl?: string, capeUrl?: string }>
|
||||
|
||||
// true means use default skin url
|
||||
private isCanvasBlank (canvas: HTMLCanvasElement): boolean {
|
||||
return !canvas.getContext('2d')
|
||||
?.getImageData(0, 0, canvas.width, canvas.height).data
|
||||
.some(channel => channel !== 0)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
updatePlayerSkin (entityId: string | number, username: string | undefined, uuid: string | undefined, skinUrl: string | true, capeUrl: string | true | undefined = undefined) {
|
||||
if (uuid) {
|
||||
if (typeof skinUrl === 'string' || typeof capeUrl === 'string') this.uuidPerSkinUrlsCache[uuid] = {}
|
||||
|
|
@ -321,75 +339,26 @@ export class Entities extends EventEmitter {
|
|||
capeUrl ??= this.uuidPerSkinUrlsCache[uuid]?.capeUrl
|
||||
}
|
||||
|
||||
let playerObject = this.getPlayerObject(entityId)
|
||||
const playerObject = this.getPlayerObject(entityId)
|
||||
if (!playerObject) return
|
||||
// const username = this.entities[entityId].username
|
||||
// or https://mulv.vercel.app/
|
||||
|
||||
if (skinUrl === true) {
|
||||
skinUrl = `https://mulv.tycrek.dev/api/lookup?username=${username}&type=skin`
|
||||
if (!username) return
|
||||
skinUrl = getLookupUrl(username, 'skin')
|
||||
}
|
||||
loadImage(skinUrl).then(image => {
|
||||
playerObject = this.getPlayerObject(entityId)
|
||||
if (!playerObject) return
|
||||
/** @type {THREE.CanvasTexture} */
|
||||
let skinTexture
|
||||
if (skinUrl === stevePng && this.defaultSteveTexture) {
|
||||
skinTexture = this.defaultSteveTexture
|
||||
} else {
|
||||
const skinCanvas = document.createElement('canvas')
|
||||
loadSkinToCanvas(skinCanvas, image)
|
||||
skinTexture = new THREE.CanvasTexture(skinCanvas)
|
||||
if (skinUrl === stevePng) {
|
||||
this.defaultSteveTexture = skinTexture
|
||||
|
||||
if (typeof skinUrl !== 'string') throw new Error('Invalid skin url')
|
||||
const renderEars = this.viewer.world.config.renderEars || username === 'deadmau5'
|
||||
void this.loadAndApplySkin(entityId, skinUrl, renderEars).then(() => {
|
||||
if (capeUrl) {
|
||||
if (capeUrl === true && username) {
|
||||
capeUrl = getLookupUrl(username, 'cape')
|
||||
}
|
||||
if (typeof capeUrl === 'string') {
|
||||
void this.loadAndApplyCape(entityId, capeUrl)
|
||||
}
|
||||
}
|
||||
skinTexture.magFilter = THREE.NearestFilter
|
||||
skinTexture.minFilter = THREE.NearestFilter
|
||||
skinTexture.needsUpdate = true
|
||||
playerObject.skin.map = skinTexture
|
||||
playerObject.skin.modelType = inferModelType(skinTexture.image)
|
||||
|
||||
const earsCanvas = document.createElement('canvas')
|
||||
loadEarsToCanvasFromSkin(earsCanvas, image)
|
||||
if (isCanvasBlank(earsCanvas)) {
|
||||
playerObject.ears.map = null
|
||||
playerObject.ears.visible = false
|
||||
} else {
|
||||
const earsTexture = new THREE.CanvasTexture(earsCanvas)
|
||||
earsTexture.magFilter = THREE.NearestFilter
|
||||
earsTexture.minFilter = THREE.NearestFilter
|
||||
earsTexture.needsUpdate = true
|
||||
//@ts-expect-error
|
||||
playerObject.ears.map = earsTexture
|
||||
playerObject.ears.visible = true
|
||||
}
|
||||
this.onSkinUpdate?.()
|
||||
if (capeUrl) {
|
||||
if (capeUrl === true) capeUrl = `https://mulv.tycrek.dev/api/lookup?username=${username}&type=cape`
|
||||
loadImage(capeUrl).then(capeImage => {
|
||||
playerObject = this.getPlayerObject(entityId)
|
||||
if (!playerObject) return
|
||||
const capeCanvas = document.createElement('canvas')
|
||||
loadCapeToCanvas(capeCanvas, capeImage)
|
||||
|
||||
const capeTexture = new THREE.CanvasTexture(capeCanvas)
|
||||
capeTexture.magFilter = THREE.NearestFilter
|
||||
capeTexture.minFilter = THREE.NearestFilter
|
||||
capeTexture.needsUpdate = true
|
||||
//@ts-expect-error
|
||||
playerObject.cape.map = capeTexture
|
||||
playerObject.cape.visible = true
|
||||
//@ts-expect-error
|
||||
playerObject.elytra.map = capeTexture
|
||||
this.onSkinUpdate?.()
|
||||
|
||||
if (!playerObject.backEquipment) {
|
||||
playerObject.backEquipment = 'cape'
|
||||
}
|
||||
}, () => { })
|
||||
}
|
||||
}, () => { })
|
||||
})
|
||||
|
||||
|
||||
playerObject.cape.visible = false
|
||||
|
|
@ -401,11 +370,93 @@ export class Entities extends EventEmitter {
|
|||
}
|
||||
playerObject.cape.map = null
|
||||
}
|
||||
}
|
||||
|
||||
function isCanvasBlank (canvas) {
|
||||
return !canvas.getContext('2d')
|
||||
.getImageData(0, 0, canvas.width, canvas.height).data
|
||||
.some(channel => channel !== 0)
|
||||
private async loadAndApplySkin (entityId: string | number, skinUrl: string, renderEars: boolean) {
|
||||
let playerObject = this.getPlayerObject(entityId)
|
||||
if (!playerObject) return
|
||||
|
||||
try {
|
||||
let playerCustomSkinImage: HTMLImageElement | undefined
|
||||
|
||||
playerObject = this.getPlayerObject(entityId)
|
||||
if (!playerObject) return
|
||||
|
||||
let skinTexture: THREE.Texture
|
||||
let skinCanvas: HTMLCanvasElement
|
||||
if (skinUrl === stevePngUrl) {
|
||||
skinTexture = await steveTexture
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) throw new Error('Failed to get context')
|
||||
ctx.drawImage(skinTexture.image, 0, 0)
|
||||
skinCanvas = canvas
|
||||
} else {
|
||||
const { canvas, image } = await loadSkinImage(skinUrl)
|
||||
playerCustomSkinImage = image
|
||||
skinTexture = new THREE.CanvasTexture(canvas)
|
||||
skinCanvas = canvas
|
||||
}
|
||||
|
||||
skinTexture.magFilter = THREE.NearestFilter
|
||||
skinTexture.minFilter = THREE.NearestFilter
|
||||
skinTexture.needsUpdate = true
|
||||
playerObject.skin.map = skinTexture as any
|
||||
playerObject.skin.modelType = inferModelType(skinCanvas)
|
||||
|
||||
let earsCanvas: HTMLCanvasElement | undefined
|
||||
if (!playerCustomSkinImage) {
|
||||
renderEars = false
|
||||
} else if (renderEars) {
|
||||
earsCanvas = document.createElement('canvas')
|
||||
loadEarsToCanvasFromSkin(earsCanvas, playerCustomSkinImage)
|
||||
renderEars = !this.isCanvasBlank(earsCanvas)
|
||||
}
|
||||
if (renderEars) {
|
||||
const earsTexture = new THREE.CanvasTexture(earsCanvas!)
|
||||
earsTexture.magFilter = THREE.NearestFilter
|
||||
earsTexture.minFilter = THREE.NearestFilter
|
||||
earsTexture.needsUpdate = true
|
||||
//@ts-expect-error
|
||||
playerObject.ears.map = earsTexture
|
||||
playerObject.ears.visible = true
|
||||
} else {
|
||||
playerObject.ears.map = null
|
||||
playerObject.ears.visible = false
|
||||
}
|
||||
this.onSkinUpdate?.()
|
||||
} catch (error) {
|
||||
console.error('Error loading skin:', error)
|
||||
}
|
||||
}
|
||||
|
||||
private async loadAndApplyCape (entityId: string | number, capeUrl: string) {
|
||||
let playerObject = this.getPlayerObject(entityId)
|
||||
if (!playerObject) return
|
||||
|
||||
try {
|
||||
const { canvas: capeCanvas, image: capeImage } = await loadSkinImage(capeUrl)
|
||||
|
||||
playerObject = this.getPlayerObject(entityId)
|
||||
if (!playerObject) return
|
||||
|
||||
loadCapeToCanvas(capeCanvas, capeImage)
|
||||
const capeTexture = new THREE.CanvasTexture(capeCanvas)
|
||||
capeTexture.magFilter = THREE.NearestFilter
|
||||
capeTexture.minFilter = THREE.NearestFilter
|
||||
capeTexture.needsUpdate = true
|
||||
//@ts-expect-error
|
||||
playerObject.cape.map = capeTexture
|
||||
playerObject.cape.visible = true
|
||||
//@ts-expect-error
|
||||
playerObject.elytra.map = capeTexture
|
||||
this.onSkinUpdate?.()
|
||||
|
||||
if (!playerObject.backEquipment) {
|
||||
playerObject.backEquipment = 'cape'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading cape:', error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -447,11 +498,11 @@ export class Entities extends EventEmitter {
|
|||
return typeof component === 'string' ? component : component.text ?? ''
|
||||
}
|
||||
|
||||
getItemMesh (item, isDropped = false) {
|
||||
const textureUv = this.getItemUv?.(item)
|
||||
getItemMesh (item, specificProps: ItemSpecificContextProperties) {
|
||||
const textureUv = this.getItemUv?.(item, specificProps)
|
||||
if (textureUv && 'resolvedModel' in textureUv) {
|
||||
const mesh = getBlockMeshFromModel(this.viewer.world.material, textureUv.resolvedModel, textureUv.modelName)
|
||||
if (isDropped) {
|
||||
if (specificProps['minecraft:display_context'] === 'ground') {
|
||||
const SCALE = 0.5
|
||||
mesh.scale.set(SCALE, SCALE, SCALE)
|
||||
mesh.position.set(0, 0.2, 0)
|
||||
|
|
@ -547,7 +598,9 @@ export class Entities extends EventEmitter {
|
|||
if (entity.name === 'item') {
|
||||
const item = entity.metadata?.find((m: any) => typeof m === 'object' && m?.itemCount)
|
||||
if (item) {
|
||||
const object = this.getItemMesh(item, true)
|
||||
const object = this.getItemMesh(item, {
|
||||
'minecraft:display_context': 'ground',
|
||||
})
|
||||
if (object) {
|
||||
mesh = object.mesh
|
||||
mesh.scale.set(0.5, 0.5, 0.5)
|
||||
|
|
@ -627,12 +680,14 @@ export class Entities extends EventEmitter {
|
|||
this.viewer.scene.add(group)
|
||||
|
||||
e = group
|
||||
e.name = 'entity'
|
||||
e['realName'] = entity.name
|
||||
this.entities[entity.id] = e
|
||||
|
||||
this.emit('add', entity)
|
||||
|
||||
if (isPlayerModel) {
|
||||
this.updatePlayerSkin(entity.id, entity.username, entity.uuid, overrides?.texture || stevePng)
|
||||
this.updatePlayerSkin(entity.id, entity.username, entity.uuid, overrides?.texture || stevePngUrl)
|
||||
}
|
||||
this.setDebugMode(this.debugMode, group)
|
||||
this.setRendering(this.rendering, group)
|
||||
|
|
@ -659,7 +714,13 @@ export class Entities extends EventEmitter {
|
|||
}
|
||||
}
|
||||
// ---
|
||||
// not player
|
||||
// set baby size
|
||||
if (meta.baby) {
|
||||
e.scale.set(0.5, 0.5, 0.5)
|
||||
} else {
|
||||
e.scale.set(1, 1, 1)
|
||||
}
|
||||
// entity specific meta
|
||||
const textDisplayMeta = getSpecificEntityMetadata('text_display', entity)
|
||||
const displayTextRaw = textDisplayMeta?.text || meta.custom_name_visible && meta.custom_name
|
||||
const displayText = this.parseEntityLabel(displayTextRaw)
|
||||
|
|
@ -781,7 +842,9 @@ export class Entities extends EventEmitter {
|
|||
mesh.scale.set(16 / 12, 16 / 12, 1)
|
||||
this.addMapModel(e, mapNumber, rotation)
|
||||
} else {
|
||||
const itemMesh = this.getItemMesh(item)
|
||||
const itemMesh = this.getItemMesh(item, {
|
||||
'minecraft:display_context': 'fixed',
|
||||
})
|
||||
if (itemMesh) {
|
||||
itemMesh.mesh.position.set(0, 0, 0.43)
|
||||
itemMesh.mesh.scale.set(0.5, 0.5, 0.5)
|
||||
|
|
@ -849,7 +912,7 @@ export class Entities extends EventEmitter {
|
|||
|
||||
mapMesh.rotation.set(0, Math.PI, 0)
|
||||
entityMesh.add(mapMesh)
|
||||
let isInvisible = false
|
||||
let isInvisible = true
|
||||
entityMesh.traverseVisible(c => {
|
||||
if (c.name === 'geometry_frame') {
|
||||
isInvisible = false
|
||||
|
|
@ -945,16 +1008,16 @@ function addArmorModel (entityMesh: THREE.Object3D, slotType: string, item: Item
|
|||
console.error('Error decoding player head texture:', err)
|
||||
}
|
||||
} else {
|
||||
texturePath = stevePng
|
||||
texturePath = stevePngUrl
|
||||
}
|
||||
}
|
||||
const armorMaterial = itemParts[0]
|
||||
if (!texturePath) {
|
||||
// TODO: Support resource pack
|
||||
// TODO: Support mirroring on certain parts of the model
|
||||
texturePath = armorModels[`${armorMaterial}Layer${layer}${overlay ? 'Overlay' : ''}`]
|
||||
const armorTextureName = `${armorMaterial}_layer_${layer}${overlay ? '_overlay' : ''}`
|
||||
texturePath = viewer.world.customTextures.armor?.textures[armorTextureName]?.src ?? armorTextures[armorTextureName]
|
||||
}
|
||||
if (!texturePath || !armorModels.armorModel[slotType]) {
|
||||
if (!texturePath || !armorModel[slotType]) {
|
||||
removeArmorModel(entityMesh, slotType)
|
||||
return
|
||||
}
|
||||
|
|
@ -973,7 +1036,7 @@ function addArmorModel (entityMesh: THREE.Object3D, slotType: string, item: Item
|
|||
material.map = texture
|
||||
})
|
||||
} else {
|
||||
mesh = getMesh(viewer.world, texturePath, armorModels.armorModel[slotType])
|
||||
mesh = getMesh(viewer.world, texturePath, armorModel[slotType])
|
||||
mesh.name = meshName
|
||||
material = mesh.material
|
||||
if (!isPlayerHead) {
|
||||
|
|
@ -991,6 +1054,8 @@ function addArmorModel (entityMesh: THREE.Object3D, slotType: string, item: Item
|
|||
material.color.setHex(0xB5_6D_51) // default brown color
|
||||
}
|
||||
addArmorModel(entityMesh, slotType, item, layer, true)
|
||||
} else {
|
||||
material.color.setHex(0xFF_FF_FF)
|
||||
}
|
||||
const group = new THREE.Object3D()
|
||||
group.name = `armor_${slotType}${overlay ? '_overlay' : ''}`
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@ import * as THREE from 'three'
|
|||
import { OBJLoader } from 'three-stdlib'
|
||||
import huskPng from 'mc-assets/dist/other-textures/latest/entity/zombie/husk.png'
|
||||
import { Vec3 } from 'vec3'
|
||||
import arrowTexture from '../../../../node_modules/mc-assets/dist/other-textures/1.21.2/entity/projectiles/arrow.png'
|
||||
import spectralArrowTexture from '../../../../node_modules/mc-assets/dist/other-textures/1.21.2/entity/projectiles/spectral_arrow.png'
|
||||
import tippedArrowTexture from '../../../../node_modules/mc-assets/dist/other-textures/1.21.2/entity/projectiles/tipped_arrow.png'
|
||||
import { WorldRendererCommon } from '../worldrendererCommon'
|
||||
import { loadTexture } from '../utils'
|
||||
import entities from './entities.json'
|
||||
import { externalModels } from './objModels'
|
||||
import externalTexturesJson from './externalTextures.json'
|
||||
// import { loadTexture } from globalThis.isElectron ? '../utils.electron.js' : '../utils';
|
||||
|
||||
const { loadTexture } = globalThis.isElectron ? require('../utils.electron.js') : require('../utils')
|
||||
|
||||
interface ElemFace {
|
||||
dir: [number, number, number]
|
||||
|
|
@ -150,7 +151,8 @@ function addCube (
|
|||
sameTextureForAllFaces = false,
|
||||
texWidth = 64,
|
||||
texHeight = 64,
|
||||
mirror = false
|
||||
mirror = false,
|
||||
errors: string[] = []
|
||||
): void {
|
||||
const cubeRotation = new THREE.Euler(0, 0, 0)
|
||||
if (cube.rotation) {
|
||||
|
|
@ -173,6 +175,14 @@ function addCube (
|
|||
u = (cube.uv[0] + dot(pos[3] ? u1 : u0, cube.size)) / texWidth
|
||||
v = (cube.uv[1] + dot(pos[4] ? v1 : v0, cube.size)) / texHeight
|
||||
}
|
||||
// if (isNaN(u) || isNaN(v)) {
|
||||
// errors.push(`NaN u: ${u}, v: ${v}`)
|
||||
// continue
|
||||
// }
|
||||
// if (u < 0 || u > 1 || v < 0 || v > 1) {
|
||||
// errors.push(`u: ${u}, v: ${v} out of range`)
|
||||
// continue
|
||||
// }
|
||||
|
||||
const posX = eastOrWest && mirror ? pos[0] ^ 1 : pos[0]
|
||||
const posY = pos[1]
|
||||
|
|
@ -213,22 +223,24 @@ function addCube (
|
|||
}
|
||||
|
||||
export function getMesh (
|
||||
worldRenderer: WorldRendererCommon,
|
||||
worldRenderer: WorldRendererCommon | undefined,
|
||||
texture: string,
|
||||
jsonModel: JsonModel,
|
||||
overrides: EntityOverrides = {}
|
||||
overrides: EntityOverrides = {},
|
||||
debugFlags: EntityDebugFlags = {}
|
||||
): THREE.SkinnedMesh {
|
||||
let textureWidth = jsonModel.texturewidth ?? 64
|
||||
let textureHeight = jsonModel.textureheight ?? 64
|
||||
let textureOffset
|
||||
let textureOffset: number[] | undefined
|
||||
const useBlockTexture = texture.startsWith('block:')
|
||||
const blocksTexture = worldRenderer.material.map!
|
||||
const blocksTexture = worldRenderer?.material.map
|
||||
if (useBlockTexture) {
|
||||
if (!worldRenderer) throw new Error('worldRenderer is required for block textures')
|
||||
const blockName = texture.slice(6)
|
||||
const textureInfo = worldRenderer.blocksAtlasParser!.getTextureInfo(blockName)
|
||||
if (textureInfo) {
|
||||
textureWidth = blocksTexture.image.width
|
||||
textureHeight = blocksTexture.image.height
|
||||
textureWidth = blocksTexture!.image.width
|
||||
textureHeight = blocksTexture!.image.height
|
||||
textureOffset = [textureInfo.u, textureInfo.v]
|
||||
} else {
|
||||
console.error(`Unknown block ${blockName}`)
|
||||
|
|
@ -272,7 +284,12 @@ export function getMesh (
|
|||
|
||||
if (jsonBone.cubes) {
|
||||
for (const cube of jsonBone.cubes) {
|
||||
addCube(geoData, i, bone, cube, useBlockTexture, textureWidth, textureHeight, jsonBone.mirror)
|
||||
const errors: string[] = []
|
||||
addCube(geoData, i, bone, cube, useBlockTexture, textureWidth, textureHeight, jsonBone.mirror, errors)
|
||||
if (errors.length) {
|
||||
debugFlags.errors ??= []
|
||||
debugFlags.errors.push(...errors.map(error => `Bone ${jsonBone.name}: ${error}`))
|
||||
}
|
||||
}
|
||||
}
|
||||
i++
|
||||
|
|
@ -305,12 +322,12 @@ export function getMesh (
|
|||
|
||||
if (textureOffset) {
|
||||
// todo(memory) dont clone
|
||||
const loadedTexture = blocksTexture.clone()
|
||||
const loadedTexture = blocksTexture!.clone()
|
||||
loadedTexture.offset.set(textureOffset[0], textureOffset[1])
|
||||
loadedTexture.needsUpdate = true
|
||||
material.map = loadedTexture
|
||||
} else {
|
||||
loadTexture(texture, loadedTexture => {
|
||||
void loadTexture(texture, loadedTexture => {
|
||||
if (material.map) {
|
||||
// texture is already loaded
|
||||
return
|
||||
|
|
@ -321,54 +338,65 @@ export function getMesh (
|
|||
loadedTexture.wrapS = THREE.RepeatWrapping
|
||||
loadedTexture.wrapT = THREE.RepeatWrapping
|
||||
material.map = loadedTexture
|
||||
const actualWidth = loadedTexture.image?.width
|
||||
}, () => {
|
||||
// This callback runs after the texture is fully loaded
|
||||
const actualWidth = material.map!.image.width
|
||||
if (actualWidth && textureWidth !== actualWidth) {
|
||||
loadedTexture.repeat.x = textureWidth / actualWidth
|
||||
material.map!.repeat.x = textureWidth / actualWidth
|
||||
}
|
||||
const actualHeight = loadedTexture.image?.height
|
||||
const actualHeight = material.map!.image.height
|
||||
if (actualHeight && textureHeight !== actualHeight) {
|
||||
loadedTexture.repeat.y = textureHeight / actualHeight
|
||||
material.map!.repeat.y = textureHeight / actualHeight
|
||||
}
|
||||
material.needsUpdate = true
|
||||
})
|
||||
}
|
||||
|
||||
return mesh
|
||||
}
|
||||
|
||||
export const knownNotHandled: string[] = [
|
||||
'area_effect_cloud', 'block_display',
|
||||
'chest_boat', 'end_crystal',
|
||||
'falling_block', 'furnace_minecart',
|
||||
'giant', 'glow_item_frame',
|
||||
'glow_squid', 'illusioner',
|
||||
'interaction', 'item',
|
||||
'item_display', 'item_frame',
|
||||
'lightning_bolt', 'marker',
|
||||
'painting', 'spawner_minecart',
|
||||
'spectral_arrow', 'tnt',
|
||||
'trader_llama', 'zombie_horse'
|
||||
export const rendererSpecialHandled = ['item_frame', 'item', 'player']
|
||||
|
||||
type EntityMapping = {
|
||||
pattern: string | RegExp
|
||||
target: string
|
||||
}
|
||||
|
||||
const temporaryMappings: EntityMapping[] = [
|
||||
// Exact matches
|
||||
{ pattern: 'furnace_minecart', target: 'minecart' },
|
||||
{ pattern: 'spawner_minecart', target: 'minecart' },
|
||||
{ pattern: 'chest_minecart', target: 'minecart' },
|
||||
{ pattern: 'hopper_minecart', target: 'minecart' },
|
||||
{ pattern: 'command_block_minecart', target: 'minecart' },
|
||||
{ pattern: 'tnt_minecart', target: 'minecart' },
|
||||
{ pattern: 'glow_item_frame', target: 'item_frame' },
|
||||
{ pattern: 'glow_squid', target: 'squid' },
|
||||
{ pattern: 'trader_llama', target: 'llama' },
|
||||
{ pattern: 'chest_boat', target: 'boat' },
|
||||
{ pattern: 'spectral_arrow', target: 'arrow' },
|
||||
{ pattern: 'husk', target: 'zombie' },
|
||||
{ pattern: 'zombie_horse', target: 'horse' },
|
||||
{ pattern: 'donkey', target: 'horse' },
|
||||
{ pattern: 'skeleton_horse', target: 'horse' },
|
||||
{ pattern: 'mule', target: 'horse' },
|
||||
{ pattern: 'ocelot', target: 'cat' },
|
||||
// Regex patterns
|
||||
{ pattern: /_minecraft$/, target: 'minecraft' },
|
||||
{ pattern: /_boat$/, target: 'boat' },
|
||||
{ pattern: /_raft$/, target: 'boat' },
|
||||
{ pattern: /_horse$/, target: 'horse' },
|
||||
{ pattern: /_zombie$/, target: 'zombie' },
|
||||
{ pattern: /_arrow$/, target: 'zombie' },
|
||||
]
|
||||
|
||||
export const temporaryMap: Record<string, string> = {
|
||||
'furnace_minecart': 'minecart',
|
||||
'spawner_minecart': 'minecart',
|
||||
'chest_minecart': 'minecart',
|
||||
'hopper_minecart': 'minecart',
|
||||
'command_block_minecart': 'minecart',
|
||||
'tnt_minecart': 'minecart',
|
||||
'glow_item_frame': 'item_frame',
|
||||
'glow_squid': 'squid',
|
||||
'trader_llama': 'llama',
|
||||
'chest_boat': 'boat',
|
||||
'spectral_arrow': 'arrow',
|
||||
'husk': 'zombie',
|
||||
'zombie_horse': 'horse',
|
||||
'donkey': 'horse',
|
||||
'skeleton_horse': 'horse',
|
||||
'mule': 'horse',
|
||||
'ocelot': 'cat',
|
||||
// 'falling_block': 'block',
|
||||
// 'lightning_bolt': 'lightning',
|
||||
function getEntityMapping (type: string): string | undefined {
|
||||
for (const mapping of temporaryMappings) {
|
||||
if (typeof mapping.pattern === 'string') {
|
||||
if (mapping.pattern === type) return mapping.target
|
||||
} else if (mapping.pattern.test(type)) { return mapping.target }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const getEntity = (name: string) => {
|
||||
|
|
@ -377,13 +405,15 @@ const getEntity = (name: string) => {
|
|||
|
||||
const scaleEntity: Record<string, number> = {
|
||||
zombie: 1.85,
|
||||
husk: 1.85
|
||||
husk: 1.85,
|
||||
arrow: 0.0025
|
||||
}
|
||||
|
||||
const offsetEntity: Record<string, Vec3> = {
|
||||
zombie: new Vec3(0, 1, 0),
|
||||
husk: new Vec3(0, 1, 0),
|
||||
boat: new Vec3(0, -1, 0),
|
||||
arrow: new Vec3(0, -0.9, 0)
|
||||
}
|
||||
|
||||
interface EntityGeometry {
|
||||
|
|
@ -393,39 +423,51 @@ interface EntityGeometry {
|
|||
}>;
|
||||
}
|
||||
|
||||
export type EntityDebugFlags = {
|
||||
type?: 'obj' | 'bedrock'
|
||||
tempMap?: string
|
||||
textureMap?: boolean
|
||||
errors?: string[]
|
||||
isHardcodedTexture?: boolean
|
||||
}
|
||||
|
||||
export class EntityMesh {
|
||||
mesh: THREE.Object3D
|
||||
|
||||
constructor (
|
||||
version: string,
|
||||
type: string,
|
||||
worldRenderer: WorldRendererCommon,
|
||||
overrides: EntityOverrides = {}
|
||||
worldRenderer?: WorldRendererCommon,
|
||||
overrides: EntityOverrides = {},
|
||||
debugFlags: EntityDebugFlags = {}
|
||||
) {
|
||||
const originalType = type
|
||||
const mappedValue = temporaryMap[type]
|
||||
if (mappedValue) type = mappedValue
|
||||
const mappedValue = getEntityMapping(type)
|
||||
if (mappedValue) {
|
||||
type = mappedValue
|
||||
debugFlags.tempMap = mappedValue
|
||||
}
|
||||
|
||||
if (externalModels[type]) {
|
||||
const objLoader = new OBJLoader()
|
||||
let texturePath = externalTexturesJson[type]
|
||||
if (originalType === 'zombie_horse') {
|
||||
texturePath = `textures/${version}/entity/horse/horse_zombie.png`
|
||||
const texturePathMap = {
|
||||
'zombie_horse': `textures/${version}/entity/horse/horse_zombie.png`,
|
||||
'husk': huskPng,
|
||||
'skeleton_horse': `textures/${version}/entity/horse/horse_skeleton.png`,
|
||||
'donkey': `textures/${version}/entity/horse/donkey.png`,
|
||||
'mule': `textures/${version}/entity/horse/mule.png`,
|
||||
'ocelot': `textures/${version}/entity/cat/ocelot.png`,
|
||||
'arrow': arrowTexture,
|
||||
'spectral_arrow': spectralArrowTexture,
|
||||
'tipped_arrow': tippedArrowTexture
|
||||
}
|
||||
if (originalType === 'husk') {
|
||||
texturePath = huskPng
|
||||
const tempTextureMap = texturePathMap[originalType] || texturePathMap[type]
|
||||
if (tempTextureMap) {
|
||||
debugFlags.textureMap = true
|
||||
}
|
||||
if (originalType === 'skeleton_horse') {
|
||||
texturePath = `textures/${version}/entity/horse/horse_skeleton.png`
|
||||
}
|
||||
if (originalType === 'donkey') {
|
||||
texturePath = `textures/${version}/entity/horse/donkey.png`
|
||||
}
|
||||
if (originalType === 'mule') {
|
||||
texturePath = `textures/${version}/entity/horse/mule.png`
|
||||
}
|
||||
if (originalType === 'ocelot') {
|
||||
texturePath = `textures/${version}/entity/cat/ocelot.png`
|
||||
const texturePath = tempTextureMap || externalTexturesJson[type]
|
||||
if (externalTexturesJson[type]) {
|
||||
debugFlags.isHardcodedTexture = true
|
||||
}
|
||||
if (!texturePath) throw new Error(`No texture for ${type}`)
|
||||
const texture = new THREE.TextureLoader().load(texturePath)
|
||||
|
|
@ -437,7 +479,7 @@ export class EntityMesh {
|
|||
alphaTest: 0.1
|
||||
})
|
||||
const obj = objLoader.parse(externalModels[type])
|
||||
const scale = scaleEntity[originalType]
|
||||
const scale = scaleEntity[originalType] || scaleEntity[type]
|
||||
if (scale) obj.scale.set(scale, scale, scale)
|
||||
const offset = offsetEntity[originalType]
|
||||
if (offset) obj.position.set(offset.x, offset.y, offset.z)
|
||||
|
|
@ -454,13 +496,22 @@ export class EntityMesh {
|
|||
}
|
||||
})
|
||||
this.mesh = obj
|
||||
debugFlags.type = 'obj'
|
||||
return
|
||||
}
|
||||
|
||||
if (originalType === 'arrow') {
|
||||
// overrides.textures = {
|
||||
// 'default': testArrow,
|
||||
// ...overrides.textures,
|
||||
// }
|
||||
}
|
||||
|
||||
const e = getEntity(type)
|
||||
if (!e) {
|
||||
if (knownNotHandled.includes(type)) return
|
||||
throw new Error(`Unknown entity ${type}`)
|
||||
// if (knownNotHandled.includes(type)) return
|
||||
// throw new Error(`Unknown entity ${type}`)
|
||||
return
|
||||
}
|
||||
|
||||
this.mesh = new THREE.Object3D()
|
||||
|
|
@ -472,7 +523,8 @@ export class EntityMesh {
|
|||
texture.endsWith('.png') || texture.startsWith('data:image/') || texture.startsWith('block:')
|
||||
? texture : texture + '.png',
|
||||
jsonModel,
|
||||
overrides)
|
||||
overrides,
|
||||
debugFlags)
|
||||
mesh.name = `geometry_${name}`
|
||||
this.mesh.add(mesh)
|
||||
|
||||
|
|
@ -482,10 +534,11 @@ export class EntityMesh {
|
|||
skeletonHelper.visible = false
|
||||
this.mesh.add(skeletonHelper)
|
||||
}
|
||||
debugFlags.type = 'bedrock'
|
||||
}
|
||||
|
||||
static getStaticData (name: string): { boneNames: string[] } {
|
||||
name = temporaryMap[name] || name
|
||||
name = getEntityMapping(name) || name
|
||||
if (externalModels[name]) {
|
||||
return {
|
||||
boneNames: [] // todo
|
||||
|
|
|
|||
|
|
@ -1,36 +1,35 @@
|
|||
/*
|
||||
* prismarine-web-client - prismarine-web-client
|
||||
* Copyright (C) 2024 Max Lee aka Phoenix616 (mail@moep.tv)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// TODO: replace with load from resource pack
|
||||
export { default as chainmailLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/chainmail_layer_1.png'
|
||||
export { default as chainmailLayer2 } from 'mc-assets/dist/other-textures/latest/models/armor/chainmail_layer_2.png'
|
||||
export { default as diamondLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/diamond_layer_1.png'
|
||||
export { default as diamondLayer2 } from 'mc-assets/dist/other-textures/latest/models/armor/diamond_layer_2.png'
|
||||
export { default as goldenLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/gold_layer_1.png'
|
||||
export { default as goldenLayer2 } from 'mc-assets/dist/other-textures/latest/models/armor/gold_layer_2.png'
|
||||
export { default as ironLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/iron_layer_1.png'
|
||||
export { default as ironLayer2 } from 'mc-assets/dist/other-textures/latest/models/armor/iron_layer_2.png'
|
||||
export { default as leatherLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/leather_layer_1.png'
|
||||
export { default as leatherLayer1Overlay } from 'mc-assets/dist/other-textures/latest/models/armor/leather_layer_1_overlay.png'
|
||||
export { default as leatherLayer2 } from 'mc-assets/dist/other-textures/latest/models/armor/leather_layer_2.png'
|
||||
export { default as leatherLayer2Overlay } from 'mc-assets/dist/other-textures/latest/models/armor/leather_layer_2_overlay.png'
|
||||
export { default as netheriteLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/netherite_layer_1.png'
|
||||
export { default as netheriteLayer2 } from 'mc-assets/dist/other-textures/latest/models/armor/netherite_layer_2.png'
|
||||
export { default as turtleLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/turtle_layer_1.png'
|
||||
import { default as chainmailLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/chainmail_layer_1.png'
|
||||
import { default as chainmailLayer2 } from 'mc-assets/dist/other-textures/latest/models/armor/chainmail_layer_2.png'
|
||||
import { default as diamondLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/diamond_layer_1.png'
|
||||
import { default as diamondLayer2 } from 'mc-assets/dist/other-textures/latest/models/armor/diamond_layer_2.png'
|
||||
import { default as goldenLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/gold_layer_1.png'
|
||||
import { default as goldenLayer2 } from 'mc-assets/dist/other-textures/latest/models/armor/gold_layer_2.png'
|
||||
import { default as ironLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/iron_layer_1.png'
|
||||
import { default as ironLayer2 } from 'mc-assets/dist/other-textures/latest/models/armor/iron_layer_2.png'
|
||||
import { default as leatherLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/leather_layer_1.png'
|
||||
import { default as leatherLayer1Overlay } from 'mc-assets/dist/other-textures/latest/models/armor/leather_layer_1_overlay.png'
|
||||
import { default as leatherLayer2 } from 'mc-assets/dist/other-textures/latest/models/armor/leather_layer_2.png'
|
||||
import { default as leatherLayer2Overlay } from 'mc-assets/dist/other-textures/latest/models/armor/leather_layer_2_overlay.png'
|
||||
import { default as netheriteLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/netherite_layer_1.png'
|
||||
import { default as netheriteLayer2 } from 'mc-assets/dist/other-textures/latest/models/armor/netherite_layer_2.png'
|
||||
import { default as turtleLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/turtle_layer_1.png'
|
||||
|
||||
export { default as armorModel } from './armorModels.json'
|
||||
|
||||
export const armorTextures = {
|
||||
'leather_layer_1': leatherLayer1,
|
||||
'leather_layer_1_overlay': leatherLayer1Overlay,
|
||||
'leather_layer_2': leatherLayer2,
|
||||
'leather_layer_2_overlay': leatherLayer2Overlay,
|
||||
'chainmail_layer_1': chainmailLayer1,
|
||||
'chainmail_layer_2': chainmailLayer2,
|
||||
'iron_layer_1': ironLayer1,
|
||||
'iron_layer_2': ironLayer2,
|
||||
'diamond_layer_1': diamondLayer1,
|
||||
'diamond_layer_2': diamondLayer2,
|
||||
'golden_layer_1': goldenLayer1,
|
||||
'golden_layer_2': goldenLayer2,
|
||||
'netherite_layer_1': netheriteLayer1,
|
||||
'netherite_layer_2': netheriteLayer2,
|
||||
'turtle_layer_1': turtleLayer1
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -22,7 +22,8 @@ export { default as parrot } from './models/parrot.obj'
|
|||
export { default as piglin } from './models/piglin.obj'
|
||||
export { default as pillager } from './models/pillager.obj'
|
||||
export { default as rabbit } from './models/rabbit.obj'
|
||||
// export { default as sheep } from './models/sheep.obj'
|
||||
export { default as sheep } from './models/sheep.obj'
|
||||
export { default as arrow } from './models/arrow.obj'
|
||||
export { default as shulker } from './models/shulker.obj'
|
||||
export { default as sniffer } from './models/sniffer.obj'
|
||||
export { default as spider } from './models/spider.obj'
|
||||
|
|
|
|||
60
renderer/viewer/lib/entity/models/arrow.obj
Normal file
60
renderer/viewer/lib/entity/models/arrow.obj
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# Aspose.3D Wavefront OBJ Exporter
|
||||
# Copyright 2004-2024 Aspose Pty Ltd.
|
||||
# File created: 02/12/2025 20:01:28
|
||||
|
||||
mtllib material.lib
|
||||
g Arrow
|
||||
|
||||
#
|
||||
# object Arrow
|
||||
#
|
||||
|
||||
v -160 8.146034E-06 50
|
||||
v 160 8.146034E-06 50
|
||||
v -160 -8.146034E-06 -50
|
||||
v 160 -8.146034E-06 -50
|
||||
v -160 -50 1.1920929E-05
|
||||
v 160 -50 1.1920929E-05
|
||||
v -160 50 -1.1920929E-05
|
||||
v 160 50 -1.1920929E-05
|
||||
v -140 -49.999992 50.000008
|
||||
v -140 50.000008 49.999992
|
||||
v -140 -50.000008 -49.999992
|
||||
v -140 49.999992 -50.000008
|
||||
# 12 vertices
|
||||
|
||||
vn 0 1 -1.6292068E-07
|
||||
vn 0 1 -1.6292068E-07
|
||||
vn 0 1 -1.6292068E-07
|
||||
vn 0 1 -1.6292068E-07
|
||||
vn 0 3.1391647E-07 1
|
||||
vn 0 3.1391647E-07 1
|
||||
vn 0 3.1391647E-07 1
|
||||
vn 0 3.1391647E-07 1
|
||||
vn -1 0 0
|
||||
vn -1 0 0
|
||||
vn -1 0 0
|
||||
vn -1 0 0
|
||||
# 12 vertex normals
|
||||
|
||||
vt 0 0.84375 0
|
||||
vt 0.5 1 0
|
||||
vt 0.5 1 0
|
||||
vt 0.5 0.84375 0
|
||||
vt 0 1 0
|
||||
vt 0.15625 0.84375 0
|
||||
vt 0.15625 0.6875 0
|
||||
vt 0 0.84375 0
|
||||
vt 0.5 0.84375 0
|
||||
vt 0 1 0
|
||||
vt 0 0.6875 0
|
||||
vt 0 0.84375 0
|
||||
# 12 texture coords
|
||||
|
||||
usemtl Arrow
|
||||
s 1
|
||||
f 1/1/1 2/9/2 4/2/3 3/10/4
|
||||
f 5/8/5 6/4/6 8/3/7 7/5/8
|
||||
f 9/11/9 10/7/10 12/6/11 11/12/12
|
||||
#3 polygons
|
||||
|
||||
|
|
@ -1,2 +1 @@
|
|||
export * as externalModels from './exportedModels'
|
||||
export * as armorModels from './armorModels'
|
||||
|
|
|
|||
|
|
@ -1,32 +1,21 @@
|
|||
import * as THREE from 'three'
|
||||
import { loadSkinToCanvas } from 'skinview-utils'
|
||||
import stevePng from 'mc-assets/dist/other-textures/latest/entity/player/wide/steve.png'
|
||||
import { getLookupUrl, loadSkinImage, steveTexture } from './utils/skins'
|
||||
|
||||
let steveTexture: THREE.Texture
|
||||
export const getMyHand = async (image?: string) => {
|
||||
export const getMyHand = async (image?: string, userName?: string) => {
|
||||
let newMap: THREE.Texture
|
||||
if (!image && steveTexture) {
|
||||
newMap = steveTexture
|
||||
if (!image && !userName) {
|
||||
newMap = await steveTexture
|
||||
} else {
|
||||
image ??= stevePng
|
||||
const skinCanvas = document.createElement('canvas')
|
||||
const img = new Image()
|
||||
img.src = image
|
||||
await new Promise<void>(resolve => {
|
||||
img.onload = () => {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
loadSkinToCanvas(skinCanvas, img)
|
||||
newMap = new THREE.CanvasTexture(skinCanvas)
|
||||
// newMap.flipY = false
|
||||
newMap.magFilter = THREE.NearestFilter
|
||||
newMap.minFilter = THREE.NearestFilter
|
||||
if (!image) {
|
||||
steveTexture = newMap
|
||||
image = getLookupUrl(userName!, 'skin')
|
||||
}
|
||||
const { canvas } = await loadSkinImage(image)
|
||||
newMap = new THREE.CanvasTexture(canvas)
|
||||
}
|
||||
|
||||
newMap.magFilter = THREE.NearestFilter
|
||||
newMap.minFilter = THREE.NearestFilter
|
||||
// right arm
|
||||
const box = new THREE.BoxGeometry()
|
||||
const material = new THREE.MeshStandardMaterial()
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -2,6 +2,8 @@ import { Vec3 } from 'vec3'
|
|||
import { World } from './world'
|
||||
import { getSectionGeometry, setBlockStatesData as setMesherData } from './models'
|
||||
|
||||
globalThis.structuredClone ??= (value) => JSON.parse(JSON.stringify(value))
|
||||
|
||||
if (module.require) {
|
||||
// If we are in a node environement, we need to fake some env variables
|
||||
const r = module.require
|
||||
|
|
@ -85,6 +87,7 @@ const handleMessage = data => {
|
|||
world ??= new World(data.config.version)
|
||||
world.config = { ...world.config, ...data.config }
|
||||
globalThis.world = world
|
||||
globalThis.Vec3 = Vec3
|
||||
}
|
||||
|
||||
switch (data.type) {
|
||||
|
|
@ -103,19 +106,26 @@ const handleMessage = data => {
|
|||
}
|
||||
case 'chunk': {
|
||||
world.addColumn(data.x, data.z, data.chunk)
|
||||
|
||||
if (data.customBlockModels) {
|
||||
const chunkKey = `${data.x},${data.z}`
|
||||
world.customBlockModels.set(chunkKey, data.customBlockModels)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'unloadChunk': {
|
||||
world.removeColumn(data.x, data.z)
|
||||
world.customBlockModels.delete(`${data.x},${data.z}`)
|
||||
if (Object.keys(world.columns).length === 0) softCleanup()
|
||||
|
||||
break
|
||||
}
|
||||
case 'blockUpdate': {
|
||||
const loc = new Vec3(data.pos.x, data.pos.y, data.pos.z).floored()
|
||||
world.setBlockStateId(loc, data.stateId)
|
||||
|
||||
const chunkKey = `${Math.floor(loc.x / 16) * 16},${Math.floor(loc.z / 16) * 16}`
|
||||
if (data.customBlockModels) {
|
||||
world.customBlockModels.set(chunkKey, data.customBlockModels)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'reset': {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ export const defaultMesherConfig = {
|
|||
disableSignsMapsSupport: false
|
||||
}
|
||||
|
||||
export type CustomBlockModels = {
|
||||
[blockPosKey: string]: string // blockPosKey is "x,y,z" -> model name
|
||||
}
|
||||
|
||||
export type MesherConfig = typeof defaultMesherConfig
|
||||
|
||||
export type MesherGeometryOutput = {
|
||||
|
|
@ -36,6 +40,7 @@ export type MesherGeometryOutput = {
|
|||
highestBlocks: Map<string, HighestBlockInfo>
|
||||
hadErrors: boolean
|
||||
blocksCount: number
|
||||
customBlockModels?: CustomBlockModels
|
||||
}
|
||||
|
||||
export type HighestBlockInfo = { y: number, stateId: number | undefined, biomeId: number | undefined }
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { Vec3 } from 'vec3'
|
|||
import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
|
||||
import moreBlockDataGeneratedJson from '../moreBlockDataGenerated.json'
|
||||
import legacyJson from '../../../../src/preflatMap.json'
|
||||
import { defaultMesherConfig } from './shared'
|
||||
import { defaultMesherConfig, CustomBlockModels } from './shared'
|
||||
import { INVISIBLE_BLOCKS } from './worldConstants'
|
||||
|
||||
const ignoreAoBlocks = Object.keys(moreBlockDataGeneratedJson.noOcclusions)
|
||||
|
|
@ -48,6 +48,7 @@ export class World {
|
|||
biomeCache: { [id: number]: mcData.Biome }
|
||||
preflat: boolean
|
||||
erroredBlockModel?: BlockModelPartsResolved
|
||||
customBlockModels = new Map<string, CustomBlockModels>() // chunkKey -> blockModels
|
||||
|
||||
constructor (version) {
|
||||
this.Chunk = Chunks(version) as any
|
||||
|
|
@ -126,6 +127,8 @@ export class World {
|
|||
// for easier testing
|
||||
if (!(pos instanceof Vec3)) pos = new Vec3(...pos as [number, number, number])
|
||||
const key = columnKey(Math.floor(pos.x / 16) * 16, Math.floor(pos.z / 16) * 16)
|
||||
const blockPosKey = `${pos.x},${pos.y},${pos.z}`
|
||||
const modelOverride = this.customBlockModels.get(key)?.[blockPosKey]
|
||||
|
||||
const column = this.columns[key]
|
||||
// null column means chunk not loaded
|
||||
|
|
@ -135,10 +138,15 @@ export class World {
|
|||
const locInChunk = posInChunk(loc)
|
||||
const stateId = column.getBlockStateId(locInChunk)
|
||||
|
||||
if (!this.blockCache[stateId]) {
|
||||
const cacheKey = modelOverride ? `${stateId}:${modelOverride}` : stateId
|
||||
|
||||
if (!this.blockCache[cacheKey]) {
|
||||
const b = column.getBlock(locInChunk) as unknown as WorldBlock
|
||||
if (modelOverride) {
|
||||
b.name = modelOverride
|
||||
}
|
||||
b.isCube = isCube(b.shapes)
|
||||
this.blockCache[stateId] = b
|
||||
this.blockCache[cacheKey] = b
|
||||
Object.defineProperty(b, 'position', {
|
||||
get () {
|
||||
throw new Error('position is not reliable, use pos parameter instead of block.position')
|
||||
|
|
@ -163,7 +171,7 @@ export class World {
|
|||
}
|
||||
}
|
||||
|
||||
const block = this.blockCache[stateId]
|
||||
const block = this.blockCache[cacheKey]
|
||||
|
||||
if (block.models === undefined && blockProvider) {
|
||||
if (!attr) throw new Error('attr is required')
|
||||
|
|
@ -188,10 +196,11 @@ export class World {
|
|||
}
|
||||
}
|
||||
|
||||
const useFallbackModel = this.preflat || modelOverride
|
||||
block.models = blockProvider.getAllResolvedModels0_1({
|
||||
name: block.name,
|
||||
properties: props,
|
||||
}, this.preflat)! // fixme! this is a hack (also need a setting for all versions)
|
||||
}, useFallbackModel)! // fixme! this is a hack (also need a setting for all versions)
|
||||
if (!block.models!.length) {
|
||||
if (block.name !== 'water' && block.name !== 'lava' && !INVISIBLE_BLOCKS.has(block.name)) {
|
||||
console.debug('[mesher] block to render not found', block.name, props)
|
||||
|
|
|
|||
169
renderer/viewer/lib/smoothSwitcher.ts
Normal file
169
renderer/viewer/lib/smoothSwitcher.ts
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
import * as tweenJs from '@tweenjs/tween.js'
|
||||
import { AnimationController } from './animationController'
|
||||
|
||||
export type StateProperties = Record<string, number>
|
||||
export type StateGetterFn = () => StateProperties
|
||||
export type StateSetterFn = (property: string, value: number) => void
|
||||
|
||||
// Speed in units per second for each property type
|
||||
const DEFAULT_SPEEDS = {
|
||||
x: 3000, // pixels/units per second
|
||||
y: 3000,
|
||||
z: 3000,
|
||||
rotation: Math.PI, // radians per second
|
||||
scale: 1, // scale units per second
|
||||
default: 3000 // default speed for unknown properties
|
||||
}
|
||||
|
||||
export class SmoothSwitcher {
|
||||
private readonly animationController = new AnimationController()
|
||||
// private readonly currentState: StateProperties = {}
|
||||
private readonly defaultState: StateProperties
|
||||
private readonly speeds: Record<string, number>
|
||||
public currentStateName = ''
|
||||
public transitioningToStateName = ''
|
||||
|
||||
constructor (
|
||||
public getState: StateGetterFn,
|
||||
public setState: StateSetterFn,
|
||||
speeds?: Partial<Record<string, number>>
|
||||
) {
|
||||
|
||||
// Initialize speeds with defaults and overrides
|
||||
this.speeds = { ...DEFAULT_SPEEDS }
|
||||
if (speeds) {
|
||||
Object.assign(this.speeds, speeds)
|
||||
}
|
||||
|
||||
// Store initial values
|
||||
this.defaultState = this.getState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate transition duration based on the largest property change
|
||||
*/
|
||||
private calculateDuration (newState: Partial<StateProperties>): number {
|
||||
let maxDuration = 0
|
||||
const currentState = this.getState()
|
||||
|
||||
for (const [key, targetValue] of Object.entries(newState)) {
|
||||
const currentValue = currentState[key]
|
||||
const diff = Math.abs(targetValue! - currentValue)
|
||||
const speed = this.getPropertySpeed(key)
|
||||
const duration = (diff / speed) * 1000 // Convert to milliseconds
|
||||
|
||||
maxDuration = Math.max(maxDuration, duration)
|
||||
}
|
||||
|
||||
// Ensure minimum duration of 50ms and maximum of 2000ms
|
||||
return Math.min(Math.max(maxDuration, 200), 2000)
|
||||
}
|
||||
|
||||
private getPropertySpeed (property: string): number {
|
||||
// Check for specific property speed
|
||||
if (property in this.speeds) {
|
||||
return this.speeds[property]
|
||||
}
|
||||
|
||||
// Check for property type (rotation, scale, etc.)
|
||||
if (property.toLowerCase().includes('rotation')) return this.speeds.rotation
|
||||
if (property.toLowerCase().includes('scale')) return this.speeds.scale
|
||||
if (property.toLowerCase() === 'x' || property.toLowerCase() === 'y' || property.toLowerCase() === 'z') {
|
||||
return this.speeds[property]
|
||||
}
|
||||
|
||||
return this.speeds.default
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a transition to a new state
|
||||
* @param newState Partial state - only need to specify properties that change
|
||||
* @param easing Easing function to use
|
||||
*/
|
||||
startTransition (
|
||||
newState: Partial<StateProperties>,
|
||||
stateName?: string,
|
||||
onEnd?: () => void,
|
||||
easing: (amount: number) => number = tweenJs.Easing.Linear.None,
|
||||
onCancelled?: () => void
|
||||
): void {
|
||||
if (this.isTransitioning) {
|
||||
onCancelled?.()
|
||||
this.animationController.forceFinish()
|
||||
}
|
||||
|
||||
this.transitioningToStateName = stateName ?? ''
|
||||
const state = this.getState()
|
||||
|
||||
const duration = this.calculateDuration(newState)
|
||||
// console.log('duration', duration, JSON.stringify(state), JSON.stringify(newState))
|
||||
|
||||
void this.animationController.startAnimation(() => {
|
||||
const group = new tweenJs.Group()
|
||||
new tweenJs.Tween(state, group)
|
||||
.to(newState, duration)
|
||||
.easing(easing)
|
||||
.onUpdate((obj) => {
|
||||
for (const key of Object.keys(obj)) {
|
||||
this.setState(key, obj[key])
|
||||
}
|
||||
})
|
||||
.onComplete(() => {
|
||||
this.animationController.forceFinish()
|
||||
this.currentStateName = this.transitioningToStateName
|
||||
this.transitioningToStateName = ''
|
||||
onEnd?.()
|
||||
})
|
||||
.start()
|
||||
return group
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset to default state
|
||||
*/
|
||||
reset (): void {
|
||||
this.startTransition(this.defaultState)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update the animation (should be called in your render/update loop)
|
||||
*/
|
||||
update (): void {
|
||||
this.animationController.update()
|
||||
}
|
||||
|
||||
/**
|
||||
* Force finish the current transition
|
||||
*/
|
||||
forceFinish (): void {
|
||||
this.animationController.forceFinish()
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new transition to the specified state
|
||||
*/
|
||||
transitionTo (
|
||||
newState: Partial<StateProperties>,
|
||||
stateName?: string,
|
||||
onEnd?: () => void,
|
||||
onCancelled?: () => void
|
||||
): void {
|
||||
this.startTransition(newState, stateName, onEnd, tweenJs.Easing.Linear.None, onCancelled)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current value of a property
|
||||
*/
|
||||
getCurrentValue (property: string): number {
|
||||
return this.getState()[property]
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if currently transitioning
|
||||
*/
|
||||
get isTransitioning (): boolean {
|
||||
return this.animationController.isActive
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import * as THREE from 'three'
|
||||
|
||||
export const disposeObject = (obj: THREE.Object3D) => {
|
||||
export const disposeObject = (obj: THREE.Object3D, cleanTextures = false) => {
|
||||
// not cleaning texture there as it might be used by other objects, but would be good to also do that
|
||||
if (obj instanceof THREE.Mesh) {
|
||||
obj.geometry?.dispose?.()
|
||||
|
|
@ -8,6 +8,11 @@ export const disposeObject = (obj: THREE.Object3D) => {
|
|||
}
|
||||
if (obj.children) {
|
||||
// eslint-disable-next-line unicorn/no-array-for-each
|
||||
obj.children.forEach(disposeObject)
|
||||
obj.children.forEach(child => disposeObject(child, cleanTextures))
|
||||
}
|
||||
if (cleanTextures) {
|
||||
if (obj instanceof THREE.Mesh) {
|
||||
obj.material?.map?.dispose?.()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
const path = require('path')
|
||||
const THREE = require('three')
|
||||
|
||||
const textureCache = {}
|
||||
function loadTexture(texture, cb) {
|
||||
if (!textureCache[texture]) {
|
||||
const url = path.resolve(__dirname, '../../public/' + texture)
|
||||
textureCache[texture] = new THREE.TextureLoader().load(url)
|
||||
}
|
||||
cb(textureCache[texture])
|
||||
}
|
||||
|
||||
function loadJSON(json, cb) {
|
||||
cb(require(path.resolve(__dirname, '../../public/' + json)))
|
||||
}
|
||||
|
||||
module.exports = { loadTexture, loadJSON }
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
function safeRequire(path) {
|
||||
try {
|
||||
return require(path)
|
||||
} catch (e) {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
const { loadImage } = safeRequire('node-canvas-webgl/lib')
|
||||
const path = require('path')
|
||||
const THREE = require('three')
|
||||
|
||||
const textureCache = {}
|
||||
// todo not ideal, export different functions for browser and node
|
||||
export function loadTexture(texture, cb) {
|
||||
if (process.platform === 'browser') {
|
||||
return require('./utils.web').loadTexture(texture, cb)
|
||||
}
|
||||
|
||||
if (textureCache[texture]) {
|
||||
cb(textureCache[texture])
|
||||
} else {
|
||||
loadImage(path.resolve(__dirname, '../../public/' + texture)).then(image => {
|
||||
textureCache[texture] = new THREE.CanvasTexture(image)
|
||||
cb(textureCache[texture])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function loadJSON(json, cb) {
|
||||
if (process.platform === 'browser') {
|
||||
return require('./utils.web').loadJSON(json, cb)
|
||||
}
|
||||
cb(require(path.resolve(__dirname, '../../public/' + json)))
|
||||
}
|
||||
|
||||
export const loadScript = async function (/** @type {string} */scriptSrc) {
|
||||
if (document.querySelector(`script[src="${scriptSrc}"]`)) {
|
||||
return
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const scriptElement = document.createElement('script')
|
||||
scriptElement.src = scriptSrc
|
||||
scriptElement.async = true
|
||||
|
||||
scriptElement.addEventListener('load', () => {
|
||||
resolve(scriptElement)
|
||||
})
|
||||
|
||||
scriptElement.onerror = (error) => {
|
||||
reject(new Error(error.message))
|
||||
scriptElement.remove()
|
||||
}
|
||||
|
||||
document.head.appendChild(scriptElement)
|
||||
})
|
||||
}
|
||||
47
renderer/viewer/lib/utils.ts
Normal file
47
renderer/viewer/lib/utils.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import * as THREE from 'three'
|
||||
|
||||
let textureCache: Record<string, THREE.Texture> = {}
|
||||
let imagesPromises: Record<string, Promise<THREE.Texture>> = {}
|
||||
|
||||
export async function loadTexture (texture: string, cb: (texture: THREE.Texture) => void, onLoad?: () => void): Promise<void> {
|
||||
const cached = textureCache[texture]
|
||||
if (!cached) {
|
||||
const { promise, resolve } = Promise.withResolvers<THREE.Texture>()
|
||||
textureCache[texture] = new THREE.TextureLoader().load(texture, resolve)
|
||||
imagesPromises[texture] = promise
|
||||
}
|
||||
|
||||
cb(textureCache[texture])
|
||||
void imagesPromises[texture].then(() => {
|
||||
onLoad?.()
|
||||
})
|
||||
}
|
||||
|
||||
export const clearTextureCache = () => {
|
||||
textureCache = {}
|
||||
imagesPromises = {}
|
||||
}
|
||||
|
||||
export const loadScript = async function (scriptSrc: string): Promise<HTMLScriptElement> {
|
||||
const existingScript = document.querySelector<HTMLScriptElement>(`script[src="${scriptSrc}"]`)
|
||||
if (existingScript) {
|
||||
return existingScript
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const scriptElement = document.createElement('script')
|
||||
scriptElement.src = scriptSrc
|
||||
scriptElement.async = true
|
||||
|
||||
scriptElement.addEventListener('load', () => {
|
||||
resolve(scriptElement)
|
||||
})
|
||||
|
||||
scriptElement.onerror = (error) => {
|
||||
reject(new Error(typeof error === 'string' ? error : (error as any).message))
|
||||
scriptElement.remove()
|
||||
}
|
||||
|
||||
document.head.appendChild(scriptElement)
|
||||
})
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
/* global XMLHttpRequest */
|
||||
const THREE = require('three')
|
||||
|
||||
const textureCache = {}
|
||||
function loadTexture(texture, cb, onLoad) {
|
||||
const cached = textureCache[texture]
|
||||
if (!cached) {
|
||||
textureCache[texture] = new THREE.TextureLoader().load(texture, onLoad)
|
||||
}
|
||||
cb(textureCache[texture])
|
||||
if (cached) onLoad?.()
|
||||
}
|
||||
|
||||
function loadJSON(url, callback) {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open('GET', url, true)
|
||||
xhr.responseType = 'json'
|
||||
xhr.onload = function () {
|
||||
const { status } = xhr
|
||||
if (status === 200) {
|
||||
callback(xhr.response)
|
||||
} else {
|
||||
throw new Error(url + ' not found')
|
||||
}
|
||||
}
|
||||
xhr.send()
|
||||
}
|
||||
|
||||
module.exports = { loadTexture, loadJSON }
|
||||
23
renderer/viewer/lib/utils/proxy.ts
Normal file
23
renderer/viewer/lib/utils/proxy.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { subscribeKey } from 'valtio/utils'
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
export function watchProperty<T extends Record<string, any>, K> (asyncGetter: (value: T[keyof T]) => Promise<K>, valtioProxy: T, key: keyof T, readySetter: (res: K) => void, cleanup?: (res: K) => void) {
|
||||
let i = 0
|
||||
let lastRes: K | undefined
|
||||
const request = async () => {
|
||||
const req = ++i
|
||||
const res = await asyncGetter(valtioProxy[key])
|
||||
if (req === i) {
|
||||
if (lastRes) {
|
||||
cleanup?.(lastRes)
|
||||
}
|
||||
readySetter(res)
|
||||
lastRes = res
|
||||
} else {
|
||||
// rejected
|
||||
cleanup?.(res)
|
||||
}
|
||||
}
|
||||
void request()
|
||||
return subscribeKey(valtioProxy, key, request)
|
||||
}
|
||||
27
renderer/viewer/lib/utils/skins.ts
Normal file
27
renderer/viewer/lib/utils/skins.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { loadSkinToCanvas } from 'skinview-utils'
|
||||
import * as THREE from 'three'
|
||||
import stevePng from 'mc-assets/dist/other-textures/latest/entity/player/wide/steve.png'
|
||||
|
||||
// eslint-disable-next-line unicorn/prefer-export-from
|
||||
export const stevePngUrl = stevePng
|
||||
export const steveTexture = new THREE.TextureLoader().loadAsync(stevePng)
|
||||
|
||||
export async function loadImageFromUrl (imageUrl: string): Promise<HTMLImageElement> {
|
||||
const img = new Image()
|
||||
img.src = imageUrl
|
||||
await new Promise<void>(resolve => {
|
||||
img.onload = () => resolve()
|
||||
})
|
||||
return img
|
||||
}
|
||||
|
||||
export function getLookupUrl (username: string, type: 'skin' | 'cape'): string {
|
||||
return `https://mulv.tycrek.dev/api/lookup?username=${username}&type=${type}`
|
||||
}
|
||||
|
||||
export async function loadSkinImage (skinUrl: string): Promise<{ canvas: HTMLCanvasElement, image: HTMLImageElement }> {
|
||||
const image = await loadImageFromUrl(skinUrl)
|
||||
const skinCanvas = document.createElement('canvas')
|
||||
loadSkinToCanvas(skinCanvas, image)
|
||||
return { canvas: skinCanvas, image }
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ import * as THREE from 'three'
|
|||
import { Vec3 } from 'vec3'
|
||||
import { generateSpiralMatrix } from 'flying-squid/dist/utils'
|
||||
import worldBlockProvider from 'mc-assets/dist/worldBlockProvider'
|
||||
import stevePng from 'mc-assets/dist/other-textures/latest/entity/player/wide/steve.png'
|
||||
import { Entities } from './entities'
|
||||
import { Primitives } from './primitives'
|
||||
import { WorldRendererThree } from './worldrendererThree'
|
||||
|
|
@ -11,6 +10,8 @@ import { WorldRendererCommon, WorldRendererConfig, defaultWorldRendererConfig }
|
|||
import { getThreeBlockModelGroup, renderBlockThree, setBlockPosition } from './mesher/standaloneRenderer'
|
||||
import { addNewStat } from './ui/newStats'
|
||||
import { getMyHand } from './hand'
|
||||
import { IPlayerState, BasePlayerState } from './basePlayerState'
|
||||
import { CameraBobbing } from './cameraBobbing'
|
||||
|
||||
export class Viewer {
|
||||
scene: THREE.Scene
|
||||
|
|
@ -21,14 +22,12 @@ export class Viewer {
|
|||
// primitives: Primitives
|
||||
domElement: HTMLCanvasElement
|
||||
playerHeight = 1.62
|
||||
isSneaking = false
|
||||
threeJsWorld: WorldRendererThree
|
||||
cameraObjectOverride?: THREE.Object3D // for xr
|
||||
audioListener: THREE.AudioListener
|
||||
renderingUntilNoUpdates = false
|
||||
processEntityOverrides = (e, overrides) => overrides
|
||||
|
||||
getMineflayerBot (): void | Record<string, any> {} // to be overridden
|
||||
private readonly cameraBobbing: CameraBobbing
|
||||
|
||||
get camera () {
|
||||
return this.world.camera
|
||||
|
|
@ -38,18 +37,19 @@ export class Viewer {
|
|||
this.world.camera = camera
|
||||
}
|
||||
|
||||
constructor (public renderer: THREE.WebGLRenderer, worldConfig = defaultWorldRendererConfig) {
|
||||
constructor (public renderer: THREE.WebGLRenderer, worldConfig = defaultWorldRendererConfig, public playerState: IPlayerState = new BasePlayerState()) {
|
||||
// https://discourse.threejs.org/t/updates-to-color-management-in-three-js-r152/50791
|
||||
THREE.ColorManagement.enabled = false
|
||||
renderer.outputColorSpace = THREE.LinearSRGBColorSpace
|
||||
|
||||
this.scene = new THREE.Scene()
|
||||
this.scene.matrixAutoUpdate = false // for perf
|
||||
this.threeJsWorld = new WorldRendererThree(this.scene, this.renderer, worldConfig)
|
||||
this.threeJsWorld = new WorldRendererThree(this.scene, this.renderer, worldConfig, this.playerState)
|
||||
this.setWorld()
|
||||
this.resetScene()
|
||||
this.entities = new Entities(this)
|
||||
// this.primitives = new Primitives(this.scene, this.camera)
|
||||
this.cameraBobbing = new CameraBobbing()
|
||||
|
||||
this.domElement = renderer.domElement
|
||||
}
|
||||
|
|
@ -114,7 +114,7 @@ export class Viewer {
|
|||
})
|
||||
}
|
||||
if (!this.world.loadedChunks[`${sectionX},${sectionZ}`]) {
|
||||
console.debug('[should be unreachable] setBlockStateId called for unloaded chunk', pos)
|
||||
// console.debug('[should be unreachable] setBlockStateId called for unloaded chunk', pos)
|
||||
}
|
||||
this.world.setBlockStateId(pos, stateId)
|
||||
}
|
||||
|
|
@ -124,7 +124,6 @@ export class Viewer {
|
|||
async demoModel () {
|
||||
//@ts-expect-error
|
||||
const pos = cursorBlockRel(0, 1, 0).position
|
||||
const blockProvider = worldBlockProvider(this.world.blockstatesModels, this.world.blocksAtlasParser!.atlas, 'latest')
|
||||
|
||||
const mesh = await getMyHand()
|
||||
// mesh.rotation.y = THREE.MathUtils.degToRad(90)
|
||||
|
|
@ -139,7 +138,7 @@ export class Viewer {
|
|||
const pos = cursorBlockRel(0, 1, 0).position
|
||||
const { mesh } = this.entities.getItemMesh({
|
||||
itemId: 541,
|
||||
})!
|
||||
}, {})!
|
||||
mesh.position.set(pos.x + 0.5, pos.y + 0.5, pos.z + 0.5)
|
||||
// mesh.scale.set(0.5, 0.5, 0.5)
|
||||
const helper = new THREE.BoxHelper(mesh, 0xff_ff_00)
|
||||
|
|
@ -161,12 +160,31 @@ export class Viewer {
|
|||
|
||||
setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number) {
|
||||
const cam = this.cameraObjectOverride || this.camera
|
||||
let yOffset = this.getMineflayerBot()?.entity?.eyeHeight ?? this.playerHeight
|
||||
if (this.isSneaking) yOffset -= 0.3
|
||||
const yOffset = this.playerState.getEyeHeight()
|
||||
// if (this.playerState.isSneaking()) yOffset -= 0.3
|
||||
|
||||
this.world.camera = cam as THREE.PerspectiveCamera
|
||||
|
||||
this.world.updateCamera(pos?.offset(0, yOffset, 0) ?? null, yaw, pitch)
|
||||
|
||||
// // Update camera bobbing based on movement state
|
||||
// const velocity = this.playerState.getVelocity()
|
||||
// const movementState = this.playerState.getMovementState()
|
||||
// const isMoving = movementState === 'SPRINTING' || movementState === 'WALKING'
|
||||
// const speed = Math.hypot(velocity.x, velocity.z)
|
||||
|
||||
// // Update bobbing state
|
||||
// this.cameraBobbing.updateWalkDistance(speed)
|
||||
// this.cameraBobbing.updateBobAmount(isMoving)
|
||||
|
||||
// // Get bobbing offsets
|
||||
// const bobbing = isMoving ? this.cameraBobbing.getBobbing() : { position: { x: 0, y: 0 }, rotation: { x: 0, z: 0 } }
|
||||
|
||||
// // Apply camera position with bobbing
|
||||
// const finalPos = pos ? pos.offset(bobbing.position.x, yOffset + bobbing.position.y, 0) : null
|
||||
// this.world.updateCamera(finalPos, yaw + bobbing.rotation.x, pitch)
|
||||
|
||||
// // Apply roll rotation separately since updateCamera doesn't handle it
|
||||
// this.camera.rotation.z = bobbing.rotation.z
|
||||
}
|
||||
|
||||
playSound (position: Vec3, path: string, volume = 1, pitch = 1) {
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export class ViewerWrapper {
|
|||
if (this.globalObject.stopLoop) return
|
||||
for (const fn of beforeRenderFrame) fn()
|
||||
this.globalObject.requestAnimationFrame(this.render.bind(this))
|
||||
if (this.globalObject.stopRender || this.renderer?.xr.isPresenting || (this.stopRenderOnBlur && !this.windowFocused)) return
|
||||
if (!viewer || this.globalObject.stopRender || this.renderer?.xr.isPresenting || (this.stopRenderOnBlur && !this.windowFocused)) return
|
||||
const renderInterval = (this.windowFocused ? this.renderInterval : this.renderIntervalUnfocused) ?? this.renderInterval
|
||||
if (renderInterval) {
|
||||
this.delta += time - this.lastTime
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { Vec3 } from 'vec3'
|
|||
import { BotEvents } from 'mineflayer'
|
||||
import { getItemFromBlock } from '../../../src/chatUtils'
|
||||
import { delayedIterator } from '../../playground/shared'
|
||||
import { playerState } from '../../../src/mineflayer/playerState'
|
||||
import { chunkPos } from './simpleUtils'
|
||||
|
||||
export type ChunkPosKey = string
|
||||
|
|
@ -102,36 +103,8 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
},
|
||||
time: () => {
|
||||
this.emitter.emit('time', bot.time.timeOfDay)
|
||||
},
|
||||
heldItemChanged () {
|
||||
handChanged(false)
|
||||
},
|
||||
}
|
||||
} satisfies Partial<BotEvents>
|
||||
const handChanged = (isLeftHand: boolean) => {
|
||||
const newItem = isLeftHand ? bot.inventory.slots[45] : bot.heldItem
|
||||
if (!newItem) {
|
||||
viewer.world.onHandItemSwitch(undefined, isLeftHand)
|
||||
return
|
||||
}
|
||||
const block = loadedData.blocksByName[newItem.name]
|
||||
// todo clean types
|
||||
const blockProperties = block ? new window.PrismarineBlock(block.id, 'void', newItem.metadata).getProperties() : {}
|
||||
// todo item props
|
||||
viewer.world.onHandItemSwitch({
|
||||
name: newItem.name,
|
||||
properties: blockProperties,
|
||||
id: newItem.type,
|
||||
type: block ? 'block' : 'item',
|
||||
fullItem: newItem,
|
||||
}, isLeftHand)
|
||||
}
|
||||
bot.inventory.on('updateSlot', (index) => {
|
||||
if (index === 45) {
|
||||
handChanged(true)
|
||||
}
|
||||
})
|
||||
handChanged(false)
|
||||
handChanged(true)
|
||||
|
||||
|
||||
bot._client.on('update_light', ({ chunkX, chunkZ }) => {
|
||||
|
|
|
|||
|
|
@ -9,15 +9,17 @@ import blocksAtlasLegacy from 'mc-assets/dist/blocksAtlasLegacy.png'
|
|||
import itemsAtlases from 'mc-assets/dist/itemsAtlases.json'
|
||||
import itemsAtlasLatest from 'mc-assets/dist/itemsAtlasLatest.png'
|
||||
import itemsAtlasLegacy from 'mc-assets/dist/itemsAtlasLegacy.png'
|
||||
import { AtlasParser } from 'mc-assets'
|
||||
import { AtlasParser, getLoadedItemDefinitionsStore } from 'mc-assets'
|
||||
import TypedEmitter from 'typed-emitter'
|
||||
import { LineMaterial } from 'three-stdlib'
|
||||
import christmasPack from 'mc-assets/dist/textureReplacements/christmas'
|
||||
import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer'
|
||||
import itemDefinitionsJson from 'mc-assets/dist/itemDefinitions.json'
|
||||
import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
|
||||
import { dynamicMcDataFiles } from '../../buildMesherConfig.mjs'
|
||||
import { toMajorVersion } from '../../../src/utils'
|
||||
import { buildCleanupDecorator } from './cleanupDecorator'
|
||||
import { defaultMesherConfig, HighestBlockInfo, MesherGeometryOutput } from './mesher/shared'
|
||||
import { defaultMesherConfig, HighestBlockInfo, MesherGeometryOutput, CustomBlockModels } from './mesher/shared'
|
||||
import { chunkPos } from './simpleUtils'
|
||||
import { HandItemBlock } from './holdingBlock'
|
||||
import { updateStatText } from './ui/newStats'
|
||||
|
|
@ -33,8 +35,10 @@ export const defaultWorldRendererConfig = {
|
|||
showChunkBorders: false,
|
||||
numWorkers: 4,
|
||||
isPlayground: false,
|
||||
renderEars: true,
|
||||
// game renderer setting actually
|
||||
displayHand: false
|
||||
showHand: false,
|
||||
viewBobbing: false
|
||||
}
|
||||
|
||||
export type WorldRendererConfig = typeof defaultWorldRendererConfig
|
||||
|
|
@ -119,12 +123,15 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
|
||||
sourceData = {
|
||||
blocksAtlases,
|
||||
itemsAtlases
|
||||
itemsAtlases,
|
||||
itemDefinitionsJson
|
||||
}
|
||||
customTextures: {
|
||||
items?: CustomTexturesData
|
||||
blocks?: CustomTexturesData
|
||||
armor?: CustomTexturesData
|
||||
} = {}
|
||||
itemsDefinitionsStore = getLoadedItemDefinitionsStore(this.sourceData.itemDefinitionsJson)
|
||||
workersProcessAverageTime = 0
|
||||
workersProcessAverageTimeCount = 0
|
||||
maxWorkersProcessTime = 0
|
||||
|
|
@ -145,7 +152,10 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
@worldCleanup()
|
||||
itemsRenderer: ItemsRenderer | undefined
|
||||
|
||||
customBlockModels = new Map<string, CustomBlockModels>()
|
||||
|
||||
abstract outputFormat: 'threeJs' | 'webgpu'
|
||||
worldBlockProvider: WorldBlockProvider
|
||||
|
||||
abstract changeBackgroundColor (color: [number, number, number]): void
|
||||
|
||||
|
|
@ -179,12 +189,18 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
this.geometryReceiveCount[data.workerIndex] ??= 0
|
||||
this.geometryReceiveCount[data.workerIndex]++
|
||||
const geometry = data.geometry as MesherGeometryOutput
|
||||
for (const key in geometry.highestBlocks) {
|
||||
const highest = geometry.highestBlocks[key]
|
||||
if (!this.highestBlocks[key] || this.highestBlocks[key].y < highest.y) {
|
||||
this.highestBlocks[key] = highest
|
||||
for (const [key, highest] of geometry.highestBlocks.entries()) {
|
||||
const currHighest = this.highestBlocks.get(key)
|
||||
if (!currHighest || currHighest.y < highest.y) {
|
||||
this.highestBlocks.set(key, highest)
|
||||
}
|
||||
}
|
||||
// for (const key in geometry.highestBlocks) {
|
||||
// const highest = geometry.highestBlocks[key]
|
||||
// if (!this.highestBlocks[key] || this.highestBlocks[key].y < highest.y) {
|
||||
// this.highestBlocks[key] = highest
|
||||
// }
|
||||
// }
|
||||
const chunkCoords = data.key.split(',').map(Number)
|
||||
this.lastChunkDistance = Math.max(...this.getDistance(new Vec3(chunkCoords[0], 0, chunkCoords[2])))
|
||||
}
|
||||
|
|
@ -204,6 +220,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
return x === chunkCoords[0] && z === chunkCoords[2]
|
||||
})) {
|
||||
this.finishedChunks[`${chunkCoords[0]},${chunkCoords[2]}`] = true
|
||||
this.renderUpdateEmitter.emit(`chunkFinished`, `${chunkCoords[0] / 16},${chunkCoords[2] / 16}`)
|
||||
}
|
||||
}
|
||||
this.checkAllFinished()
|
||||
|
|
@ -240,7 +257,6 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
}
|
||||
}
|
||||
|
||||
onHandItemSwitch (item: HandItemBlock | undefined, isLeftHand: boolean): void { }
|
||||
changeHandSwingingState (isAnimationPlaying: boolean, isLeftHand: boolean): void { }
|
||||
|
||||
abstract handleWorkerMessage (data: WorkerReceive): void
|
||||
|
|
@ -304,7 +320,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
this.mesherConfig.version = this.version!
|
||||
|
||||
this.sendMesherMcData()
|
||||
await this.updateTexturesData()
|
||||
await this.updateAssetsData()
|
||||
}
|
||||
|
||||
sendMesherMcData () {
|
||||
|
|
@ -321,7 +337,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
}
|
||||
}
|
||||
|
||||
async updateTexturesData (resourcePackUpdate = false, prioritizeBlockTextures?: string[]) {
|
||||
async updateAssetsData (resourcePackUpdate = false, prioritizeBlockTextures?: string[]) {
|
||||
const blocksAssetsParser = new AtlasParser(this.sourceData.blocksAtlases, blocksAtlasLatest, blocksAtlasLegacy)
|
||||
const itemsAssetsParser = new AtlasParser(this.sourceData.itemsAtlases, itemsAtlasLatest, itemsAtlasLegacy)
|
||||
|
||||
|
|
@ -345,6 +361,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
this.itemsAtlasParser = new AtlasParser({ latest: itemsAtlas }, itemsCanvas.toDataURL())
|
||||
|
||||
this.itemsRenderer = new ItemsRenderer(this.version!, this.blockstatesModels, this.itemsAtlasParser, this.blocksAtlasParser)
|
||||
this.worldBlockProvider = worldBlockProvider(this.blockstatesModels, this.blocksAtlasParser.atlas, 'latest')
|
||||
|
||||
const texture = await new THREE.TextureLoader().loadAsync(this.blocksAtlasParser.latestImage)
|
||||
texture.magFilter = THREE.NearestFilter
|
||||
|
|
@ -398,9 +415,18 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
this.initialChunkLoadWasStartedIn ??= Date.now()
|
||||
this.loadedChunks[`${x},${z}`] = true
|
||||
this.updateChunksStatsText()
|
||||
|
||||
const chunkKey = `${x},${z}`
|
||||
const customBlockModels = this.customBlockModels.get(chunkKey)
|
||||
|
||||
for (const worker of this.workers) {
|
||||
// todo optimize
|
||||
worker.postMessage({ type: 'chunk', x, z, chunk })
|
||||
worker.postMessage({
|
||||
type: 'chunk',
|
||||
x,
|
||||
z,
|
||||
chunk,
|
||||
customBlockModels: customBlockModels || undefined
|
||||
})
|
||||
}
|
||||
for (let y = this.worldMinYRender; y < this.worldConfig.worldHeight; y += 16) {
|
||||
const loc = new Vec3(x, y, z)
|
||||
|
|
@ -450,8 +476,17 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
|
||||
setBlockStateId (pos: Vec3, stateId: number) {
|
||||
const needAoRecalculation = true
|
||||
const chunkKey = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.z / 16) * 16}`
|
||||
const blockPosKey = `${pos.x},${pos.y},${pos.z}`
|
||||
const customBlockModels = this.customBlockModels.get(chunkKey) || {}
|
||||
|
||||
for (const worker of this.workers) {
|
||||
worker.postMessage({ type: 'blockUpdate', pos, stateId })
|
||||
worker.postMessage({
|
||||
type: 'blockUpdate',
|
||||
pos,
|
||||
stateId,
|
||||
customBlockModels
|
||||
})
|
||||
}
|
||||
this.setSectionDirty(pos, true, true)
|
||||
if (this.neighborChunkUpdates) {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { disposeObject } from './threeJsUtils'
|
|||
import HoldingBlock, { HandItemBlock } from './holdingBlock'
|
||||
import { addNewStat } from './ui/newStats'
|
||||
import { MesherGeometryOutput } from './mesher/shared'
|
||||
import { IPlayerState } from './basePlayerState'
|
||||
import { getMesh } from './entity/EntityMesh'
|
||||
import { armorModel } from './entity/armorModels'
|
||||
|
||||
|
|
@ -36,48 +37,29 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).blocksCount, 0)
|
||||
}
|
||||
|
||||
constructor (public scene: THREE.Scene, public renderer: THREE.WebGLRenderer, public config: WorldRendererConfig) {
|
||||
constructor (public scene: THREE.Scene, public renderer: THREE.WebGLRenderer, public config: WorldRendererConfig, public playerState: IPlayerState) {
|
||||
super(config)
|
||||
this.rendererDevice = `${WorldRendererThree.getRendererInfo(this.renderer)} powered by three.js r${THREE.REVISION}`
|
||||
this.starField = new StarField(scene)
|
||||
this.holdingBlock = new HoldingBlock()
|
||||
this.holdingBlockLeft = new HoldingBlock()
|
||||
this.holdingBlockLeft.rightSide = false
|
||||
this.holdingBlock = new HoldingBlock(playerState, this.config)
|
||||
this.holdingBlockLeft = new HoldingBlock(playerState, this.config, true)
|
||||
|
||||
this.renderUpdateEmitter.on('itemsTextureDownloaded', () => {
|
||||
if (this.holdingBlock.toBeRenderedItem) {
|
||||
this.onHandItemSwitch(this.holdingBlock.toBeRenderedItem)
|
||||
this.holdingBlock.toBeRenderedItem = undefined
|
||||
}
|
||||
if (this.holdingBlockLeft.toBeRenderedItem) {
|
||||
this.onHandItemSwitch(this.holdingBlock.toBeRenderedItem, true)
|
||||
this.holdingBlockLeft.toBeRenderedItem = undefined
|
||||
}
|
||||
this.holdingBlock.ready = true
|
||||
this.holdingBlock.updateItem()
|
||||
this.holdingBlockLeft.ready = true
|
||||
this.holdingBlockLeft.updateItem()
|
||||
})
|
||||
|
||||
this.addDebugOverlay()
|
||||
}
|
||||
|
||||
onHandItemSwitch (item: HandItemBlock | undefined, isLeft = false) {
|
||||
if (!isLeft) {
|
||||
item ??= {
|
||||
type: 'hand',
|
||||
}
|
||||
}
|
||||
const holdingBlock = isLeft ? this.holdingBlockLeft : this.holdingBlock
|
||||
if (!this.currentTextureImage) {
|
||||
holdingBlock.toBeRenderedItem = item
|
||||
return
|
||||
}
|
||||
void holdingBlock.initHandObject(item)
|
||||
}
|
||||
|
||||
changeHandSwingingState (isAnimationPlaying: boolean, isLeft = false) {
|
||||
const holdingBlock = isLeft ? this.holdingBlockLeft : this.holdingBlock
|
||||
if (isAnimationPlaying) {
|
||||
holdingBlock.startSwing()
|
||||
} else {
|
||||
void holdingBlock.stopSwing()
|
||||
holdingBlock.stopSwing()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -252,7 +234,7 @@ 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
|
||||
this.renderer.render(this.scene, cam)
|
||||
if (this.config.displayHand) {
|
||||
if (this.config.showHand) {
|
||||
this.holdingBlock.render(this.camera, this.renderer, viewer.ambientLight, viewer.directionalLight)
|
||||
this.holdingBlockLeft.render(this.camera, this.renderer, viewer.ambientLight, viewer.directionalLight)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
globalThis.resolveDnsFallback = async (hostname: string) => {
|
||||
const response = await fetchServerStatus(hostname)
|
||||
return response?.raw.srv_record ?? undefined
|
||||
}
|
||||
|
||||
export const isServerValid = (ip: string) => {
|
||||
const isInLocalNetwork = ip.startsWith('192.168.') ||
|
||||
ip.startsWith('10.') ||
|
||||
|
|
@ -51,4 +56,9 @@ export type ServerResponse = {
|
|||
// todo display via hammer icon
|
||||
software?: string
|
||||
plugins?: Array<{ name, version }>
|
||||
// port?: number
|
||||
srv_record?: {
|
||||
host: string
|
||||
port: number
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,15 +112,20 @@ const commands: Array<{
|
|||
{
|
||||
command: ['/pos'],
|
||||
async invoke ([type]) {
|
||||
let pos: string | undefined
|
||||
let pos: { x: number, y: number, z: number } | undefined
|
||||
if (type === 'block') {
|
||||
pos = window.cursorBlockRel()?.position?.toString().slice(1, -1)
|
||||
const blockPos = window.cursorBlockRel()?.position
|
||||
if (blockPos) {
|
||||
pos = { x: blockPos.x, y: blockPos.y, z: blockPos.z }
|
||||
}
|
||||
} else {
|
||||
pos = bot.entity.position.toString().slice(1, -1)
|
||||
const playerPos = bot.entity.position
|
||||
pos = { x: playerPos.x, y: playerPos.y, z: playerPos.z }
|
||||
}
|
||||
if (!pos) return
|
||||
await navigator.clipboard.writeText(pos)
|
||||
writeText(`Copied position to clipboard: ${pos}`)
|
||||
const formatted = `${pos.x.toFixed(2)} ${pos.y.toFixed(2)} ${pos.z.toFixed(2)}`
|
||||
await navigator.clipboard.writeText(formatted)
|
||||
writeText(`Copied position to clipboard: ${formatted}`)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -11,24 +11,6 @@ const mapIncludeDefined = (props) => {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
TODO (nbt)
|
||||
{
|
||||
"extra": [
|
||||
{
|
||||
"italic": 0,
|
||||
"underlined": 0,
|
||||
"bold": 0,
|
||||
"color": "aqua",
|
||||
"obfuscated": 0,
|
||||
"strikethrough": 0,
|
||||
"text": "minecraft:lift"
|
||||
}
|
||||
],
|
||||
"text": ""
|
||||
}
|
||||
*/
|
||||
|
||||
test('formatMessage', () => {
|
||||
const result = formatMessage({
|
||||
'json': {
|
||||
|
|
|
|||
|
|
@ -524,13 +524,18 @@ contro.on('trigger', ({ command }) => {
|
|||
if (command === 'ui.toggleFullscreen') {
|
||||
void goFullscreen(true)
|
||||
}
|
||||
})
|
||||
|
||||
if (command === 'ui.toggleMap') {
|
||||
if (activeModalStack.at(-1)?.reactType === 'full-map') {
|
||||
hideModal({ reactType: 'full-map' })
|
||||
} else {
|
||||
showModal({ reactType: 'full-map' })
|
||||
}
|
||||
// show-hide Fullmap
|
||||
contro.on('trigger', ({ command }) => {
|
||||
if (command !== 'ui.toggleMap') return
|
||||
const isActive = isGameActive(true)
|
||||
if (activeModalStack.at(-1)?.reactType === 'full-map') {
|
||||
miscUiState.displayFullmap = false
|
||||
hideModal({ reactType: 'full-map' })
|
||||
} else if (isActive && !activeModalStack.length) {
|
||||
miscUiState.displayFullmap = true
|
||||
showModal({ reactType: 'full-map' })
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
75
src/customChannels.ts
Normal file
75
src/customChannels.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { Vec3 } from 'vec3'
|
||||
import { options } from './optionsStorage'
|
||||
|
||||
customEvents.on('mineflayerBotCreated', async () => {
|
||||
if (!options.customChannels) return
|
||||
await new Promise(resolve => {
|
||||
bot.once('login', () => {
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
|
||||
const CHANNEL_NAME = 'minecraft-web-client:blockmodels'
|
||||
|
||||
const packetStructure = [
|
||||
'container',
|
||||
[
|
||||
{
|
||||
name: 'worldName', // currently not used
|
||||
type: ['pstring', { countType: 'i16' }]
|
||||
},
|
||||
{
|
||||
name: 'x',
|
||||
type: 'i32'
|
||||
},
|
||||
{
|
||||
name: 'y',
|
||||
type: 'i32'
|
||||
},
|
||||
{
|
||||
name: 'z',
|
||||
type: 'i32'
|
||||
},
|
||||
{
|
||||
name: 'model',
|
||||
type: ['pstring', { countType: 'i16' }]
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
bot._client.registerChannel(CHANNEL_NAME, packetStructure, true)
|
||||
|
||||
bot._client.on(CHANNEL_NAME as any, (data) => {
|
||||
const { worldName, x, y, z, model } = data
|
||||
console.debug('Received model data:', { worldName, x, y, z, model })
|
||||
|
||||
if (viewer?.world) {
|
||||
const chunkX = Math.floor(x / 16) * 16
|
||||
const chunkZ = Math.floor(z / 16) * 16
|
||||
const chunkKey = `${chunkX},${chunkZ}`
|
||||
const blockPosKey = `${x},${y},${z}`
|
||||
|
||||
const chunkModels = viewer.world.customBlockModels.get(chunkKey) || {}
|
||||
|
||||
if (model) {
|
||||
chunkModels[blockPosKey] = model
|
||||
} else {
|
||||
delete chunkModels[blockPosKey]
|
||||
}
|
||||
|
||||
if (Object.keys(chunkModels).length > 0) {
|
||||
viewer.world.customBlockModels.set(chunkKey, chunkModels)
|
||||
} else {
|
||||
viewer.world.customBlockModels.delete(chunkKey)
|
||||
}
|
||||
|
||||
// Trigger update
|
||||
const block = worldView!.world.getBlock(new Vec3(x, y, z))
|
||||
if (block) {
|
||||
worldView!.world.setBlockStateId(new Vec3(x, y, z), block.stateId)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
console.debug(`registered custom channel ${CHANNEL_NAME} channel`)
|
||||
})
|
||||
|
|
@ -24,6 +24,19 @@ Object.defineProperty(window, 'debugSceneChunks', {
|
|||
},
|
||||
})
|
||||
|
||||
window.chunkKey = (xRel = 0, zRel = 0) => {
|
||||
const pos = bot.entity.position
|
||||
return `${(Math.floor(pos.x / 16) + xRel) * 16},${(Math.floor(pos.z / 16) + zRel) * 16}`
|
||||
}
|
||||
|
||||
window.sectionKey = (xRel = 0, yRel = 0, zRel = 0) => {
|
||||
const pos = bot.entity.position
|
||||
return `${(Math.floor(pos.x / 16) + xRel) * 16},${(Math.floor(pos.y / 16) + yRel) * 16},${(Math.floor(pos.z / 16) + zRel) * 16}`
|
||||
}
|
||||
|
||||
window.keys = (obj) => Object.keys(obj)
|
||||
window.values = (obj) => Object.values(obj)
|
||||
|
||||
window.len = (obj) => Object.keys(obj).length
|
||||
|
||||
customEvents.on('gameLoaded', () => {
|
||||
|
|
@ -133,3 +146,17 @@ Object.defineProperty(window, 'debugToggle', {
|
|||
console.log('Enabled debug for', v)
|
||||
}
|
||||
})
|
||||
|
||||
customEvents.on('gameLoaded', () => {
|
||||
window.holdingBlock = (viewer.world as WorldRendererThree).holdingBlock
|
||||
})
|
||||
|
||||
window.clearStorage = (...keysToKeep: string[]) => {
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i)
|
||||
if (key && !keysToKeep.includes(key)) {
|
||||
localStorage.removeItem(key)
|
||||
}
|
||||
}
|
||||
return `Cleared ${localStorage.length - keysToKeep.length} items from localStorage. Kept: ${keysToKeep.join(', ')}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,16 @@
|
|||
import { proxy, ref, subscribe } from 'valtio'
|
||||
import type { WorldWarp } from 'flying-squid/dist/lib/modules/warps'
|
||||
import type { OptionsGroupType } from './optionsGuiScheme'
|
||||
import { appQueryParams } from './appParams'
|
||||
|
||||
// todo: refactor structure with support of hideNext=false
|
||||
|
||||
const notHideableModalsWithoutForce = new Set(['app-status'])
|
||||
|
||||
if (appQueryParams.lockConnect) {
|
||||
notHideableModalsWithoutForce.add('editServer')
|
||||
}
|
||||
|
||||
type Modal = ({ elem?: HTMLElement & Record<string, any> } & { reactType: string })
|
||||
|
||||
type ContextMenuItem = { callback; label }
|
||||
|
|
@ -137,6 +142,7 @@ export const miscUiState = proxy({
|
|||
usingGamepadInput: false,
|
||||
appConfig: null as AppConfig | null,
|
||||
displaySearchInput: false,
|
||||
displayFullmap: false
|
||||
})
|
||||
|
||||
export const isGameActive = (foregroundCheck: boolean) => {
|
||||
|
|
|
|||
67
src/index.ts
67
src/index.ts
|
|
@ -5,6 +5,7 @@ import './testCrasher'
|
|||
import './globals'
|
||||
import './devtools'
|
||||
import './entities'
|
||||
import './customChannels'
|
||||
import './globalDomListeners'
|
||||
import './mineflayer/maps'
|
||||
import './mineflayer/cameraShake'
|
||||
|
|
@ -66,6 +67,7 @@ import { isCypress } from './standaloneUtils'
|
|||
import {
|
||||
removePanorama
|
||||
} from './panorama'
|
||||
import { getItemDefinition } from 'mc-assets/dist/itemDefinitions'
|
||||
|
||||
import { startLocalServer, unsupportedLocalServerFeatures } from './createLocalServer'
|
||||
import defaultServerOptions from './defaultLocalServerOptions'
|
||||
|
|
@ -104,9 +106,11 @@ import { getWebsocketStream } from './mineflayer/websocket-core'
|
|||
import { appQueryParams, appQueryParamsArray } from './appParams'
|
||||
import { updateCursor } from './cameraRotationControls'
|
||||
import { pingServerVersion } from './mineflayer/minecraft-protocol-extra'
|
||||
import { playerState, PlayerStateManager } from './mineflayer/playerState'
|
||||
import { states } from 'minecraft-protocol'
|
||||
import { initMotionTracking } from './react/uiMotion'
|
||||
import { UserError } from './mineflayer/userError'
|
||||
import ping from './mineflayer/plugins/ping'
|
||||
|
||||
window.debug = debug
|
||||
window.THREE = THREE
|
||||
|
|
@ -161,21 +165,29 @@ if (isIphone) {
|
|||
if (appQueryParams.testCrashApp === '2') throw new Error('test')
|
||||
|
||||
// Create viewer
|
||||
const viewer: import('renderer/viewer/lib/viewer').Viewer = new Viewer(renderer)
|
||||
const viewer: import('renderer/viewer/lib/viewer').Viewer = new Viewer(renderer, undefined, playerState)
|
||||
window.viewer = viewer
|
||||
viewer.getMineflayerBot = () => bot
|
||||
// todo unify
|
||||
viewer.entities.getItemUv = (item) => {
|
||||
viewer.entities.getItemUv = (item, specificProps) => {
|
||||
const idOrName = item.itemId ?? item.blockId
|
||||
try {
|
||||
const name = typeof idOrName === 'number' ? loadedData.items[idOrName]?.name : idOrName
|
||||
if (!name) throw new Error(`Item not found: ${idOrName}`)
|
||||
|
||||
const renderInfo = renderSlot({
|
||||
name,
|
||||
nbt: null,
|
||||
...item
|
||||
const itemSelector = playerState.getItemSelector({
|
||||
...specificProps
|
||||
})
|
||||
const model = getItemDefinition(viewer.world.itemsDefinitionsStore, {
|
||||
name,
|
||||
version: viewer.world.texturesVersion!,
|
||||
properties: itemSelector
|
||||
})?.model ?? name
|
||||
|
||||
const renderInfo = renderSlot({
|
||||
...item,
|
||||
nbt: null,
|
||||
name: model,
|
||||
}, false, true)
|
||||
|
||||
if (!renderInfo) throw new Error(`Failed to get render info for item ${name}`)
|
||||
|
||||
|
|
@ -409,10 +421,12 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
throw err
|
||||
}
|
||||
}
|
||||
const oldStatus = appStatusState.status
|
||||
setLoadingScreenStatus('Loading minecraft assets')
|
||||
viewer.world.blockstatesModels = await import('mc-assets/dist/blockStatesModels.json')
|
||||
void viewer.setVersion(version, options.useVersionsTextures === 'latest' ? version : options.useVersionsTextures)
|
||||
miscUiState.loadedDataVersion = version
|
||||
setLoadingScreenStatus(oldStatus)
|
||||
}
|
||||
|
||||
let finalVersion = connectOptions.botVersion || (singleplayer ? serverOptions.version : undefined)
|
||||
|
|
@ -630,22 +644,6 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
})
|
||||
})
|
||||
})
|
||||
let i = 0
|
||||
//@ts-expect-error
|
||||
bot.pingProxy = async () => {
|
||||
const curI = ++i
|
||||
return new Promise(resolve => {
|
||||
//@ts-expect-error
|
||||
bot._client.socket._ws.send(`ping:${curI}`)
|
||||
const date = Date.now()
|
||||
const onPong = (received) => {
|
||||
if (received !== curI.toString()) return
|
||||
bot._client.socket.off('pong' as any, onPong)
|
||||
resolve(Date.now() - date)
|
||||
}
|
||||
bot._client.socket.on('pong' as any, onPong)
|
||||
})
|
||||
}
|
||||
}
|
||||
// socket setup actually can be delayed because of dns lookup
|
||||
if (bot._client.socket) {
|
||||
|
|
@ -663,6 +661,10 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
} catch (err) {
|
||||
handleError(err)
|
||||
}
|
||||
|
||||
if (connectOptions.server) {
|
||||
bot.loadPlugin(ping)
|
||||
}
|
||||
if (!bot) return
|
||||
|
||||
const p2pConnectTimeout = p2pMultiplayer ? setTimeout(() => { throw new UserError('Spawn timeout. There might be error on the other side, check console.') }, 20_000) : undefined
|
||||
|
|
@ -712,6 +714,13 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
worldInteractions.initBot()
|
||||
|
||||
setLoadingScreenStatus('Loading world')
|
||||
|
||||
const mcData = MinecraftData(bot.version)
|
||||
window.PrismarineBlock = PrismarineBlock(mcData.version.minecraftVersion!)
|
||||
window.PrismarineItem = PrismarineItem(mcData.version.minecraftVersion!)
|
||||
window.loadedData = mcData
|
||||
window.Vec3 = Vec3
|
||||
window.pathfinder = pathfinder
|
||||
})
|
||||
|
||||
const spawnEarlier = !singleplayer && !p2pMultiplayer
|
||||
|
|
@ -732,18 +741,14 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
}
|
||||
window.focus?.()
|
||||
errorAbortController.abort()
|
||||
const mcData = MinecraftData(bot.version)
|
||||
window.PrismarineBlock = PrismarineBlock(mcData.version.minecraftVersion!)
|
||||
window.PrismarineItem = PrismarineItem(mcData.version.minecraftVersion!)
|
||||
window.loadedData = mcData
|
||||
window.Vec3 = Vec3
|
||||
window.pathfinder = pathfinder
|
||||
|
||||
miscUiState.gameLoaded = true
|
||||
miscUiState.loadedServerIndex = connectOptions.serverIndex ?? ''
|
||||
customEvents.emit('gameLoaded')
|
||||
if (p2pConnectTimeout) clearTimeout(p2pConnectTimeout)
|
||||
|
||||
playerState.onlineMode = !!connectOptions.authenticatedAccount
|
||||
|
||||
setLoadingScreenStatus('Placing blocks (starting viewer)')
|
||||
localStorage.lastConnectOptions = JSON.stringify(connectOptions)
|
||||
connectOptions.onSuccessfulPlay?.()
|
||||
|
|
@ -997,6 +1002,4 @@ if (initialLoader) {
|
|||
}
|
||||
window.pageLoaded = true
|
||||
|
||||
if (!reconnectOptions) {
|
||||
void possiblyHandleStateVariable()
|
||||
}
|
||||
void possiblyHandleStateVariable()
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -174,7 +174,7 @@ const getImage = ({ path = undefined as string | undefined, texture = undefined
|
|||
return loadedImagesCache.get(loadPath)
|
||||
}
|
||||
|
||||
export const renderSlot = (slot: GeneralInputItem, debugIsQuickbar = false): {
|
||||
export const renderSlot = (slot: GeneralInputItem, debugIsQuickbar = false, fullBlockModelSupport = false): {
|
||||
texture: string,
|
||||
blockData?: Record<string, { slice, path }> & { resolvedModel: BlockModel },
|
||||
scale?: number,
|
||||
|
|
@ -198,7 +198,7 @@ export const renderSlot = (slot: GeneralInputItem, debugIsQuickbar = false): {
|
|||
let itemTexture
|
||||
try {
|
||||
assertDefined(viewer.world.itemsRenderer)
|
||||
itemTexture = viewer.world.itemsRenderer.getItemTexture(itemModelName) ?? viewer.world.itemsRenderer.getItemTexture('item/missing_texture')!
|
||||
itemTexture = viewer.world.itemsRenderer.getItemTexture(itemModelName, {}, false, fullBlockModelSupport) ?? viewer.world.itemsRenderer.getItemTexture('item/missing_texture')!
|
||||
} catch (err) {
|
||||
inGameError(`Failed to render item ${itemModelName} (original: ${originalItemName}) on ${bot.version} (resourcepack: ${options.enabledResourcepack}): ${err.stack}`)
|
||||
itemTexture = viewer.world.itemsRenderer!.getItemTexture('block/errored')!
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export const getItemMetadata = (item: GeneralInputItem) => {
|
|||
|
||||
const customTextComponent = componentMap.get('custom_name') || componentMap.get('item_name')
|
||||
if (customTextComponent) {
|
||||
customText = customTextComponent.data.value
|
||||
customText = nbt.simplify(customTextComponent.data)
|
||||
}
|
||||
const customModelComponent = componentMap.get('item_model')
|
||||
if (customModelComponent) {
|
||||
|
|
@ -70,6 +70,9 @@ export const getItemNameRaw = (item: Pick<import('prismarine-item').Item, 'nbt'>
|
|||
const { customText } = getItemMetadata(item as any)
|
||||
if (!customText) return
|
||||
try {
|
||||
if (typeof customText === 'object') {
|
||||
return customText
|
||||
}
|
||||
const parsed = customText.startsWith('{') && customText.endsWith('}') ? mojangson.simplify(mojangson.parse(customText)) : fromFormattedString(customText)
|
||||
if (parsed.extra) {
|
||||
return parsed as Record<string, any>
|
||||
|
|
@ -78,7 +81,7 @@ export const getItemNameRaw = (item: Pick<import('prismarine-item').Item, 'nbt'>
|
|||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
text: customText
|
||||
text: JSON.stringify(customText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ const writeCmd = (cmd: string) => {
|
|||
bot.chat(cmd)
|
||||
}
|
||||
|
||||
let msg = 0
|
||||
const LIMIT_MSG = 100
|
||||
export const javaServerTester = {
|
||||
itemCustomLore () {
|
||||
const cmd = customStickNbt({
|
||||
|
|
@ -50,5 +52,17 @@ export const javaServerTester = {
|
|||
custom_name: [{ translate: 'item.diamond.name' }]
|
||||
})
|
||||
writeCmd(cmd)
|
||||
},
|
||||
|
||||
spamChat () {
|
||||
for (let i = msg; i < msg + LIMIT_MSG; i++) {
|
||||
bot.chat('Hello, world, ' + i)
|
||||
}
|
||||
msg += LIMIT_MSG
|
||||
},
|
||||
spamChatComplexMessage () {
|
||||
for (let i = msg; i < msg + LIMIT_MSG; i++) {
|
||||
bot.chat('/tell @a ' + i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
200
src/mineflayer/playerState.ts
Normal file
200
src/mineflayer/playerState.ts
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
import { EventEmitter } from 'events'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { IPlayerState, ItemSpecificContextProperties, MovementState, PlayerStateEvents } from 'renderer/viewer/lib/basePlayerState'
|
||||
import { HandItemBlock } from 'renderer/viewer/lib/holdingBlock'
|
||||
import TypedEmitter from 'typed-emitter'
|
||||
import { ItemSelector } from 'mc-assets/dist/itemDefinitions'
|
||||
import { proxy } from 'valtio'
|
||||
import { gameAdditionalState } from '../globalState'
|
||||
|
||||
export class PlayerStateManager implements IPlayerState {
|
||||
disableStateUpdates = false
|
||||
private static instance: PlayerStateManager
|
||||
readonly events = new EventEmitter() as TypedEmitter<PlayerStateEvents>
|
||||
|
||||
// Movement and physics state
|
||||
private lastVelocity = new Vec3(0, 0, 0)
|
||||
private movementState: MovementState = 'NOT_MOVING'
|
||||
private timeOffGround = 0
|
||||
private lastUpdateTime = performance.now()
|
||||
|
||||
// Held item state
|
||||
private heldItem?: HandItemBlock
|
||||
private offHandItem?: HandItemBlock
|
||||
private itemUsageTicks = 0
|
||||
private isUsingItem = false
|
||||
private ready = false
|
||||
onlineMode = false
|
||||
get username () {
|
||||
return bot.player?.username ?? ''
|
||||
}
|
||||
|
||||
reactive = proxy({
|
||||
playerSkin: undefined as string | undefined,
|
||||
})
|
||||
|
||||
static getInstance (): PlayerStateManager {
|
||||
if (!this.instance) {
|
||||
this.instance = new PlayerStateManager()
|
||||
}
|
||||
return this.instance
|
||||
}
|
||||
|
||||
private constructor () {
|
||||
this.updateState = this.updateState.bind(this)
|
||||
customEvents.on('mineflayerBotCreated', () => {
|
||||
this.ready = false
|
||||
bot.on('inject_allowed', () => {
|
||||
if (this.ready) return
|
||||
this.ready = true
|
||||
this.botCreated()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private botCreated () {
|
||||
// Movement tracking
|
||||
bot.on('move', this.updateState)
|
||||
|
||||
// Item tracking
|
||||
bot.on('heldItemChanged', () => {
|
||||
return this.updateHeldItem(false)
|
||||
})
|
||||
bot.inventory.on('updateSlot', (index) => {
|
||||
if (index === 45) this.updateHeldItem(true)
|
||||
})
|
||||
bot.on('physicsTick', () => {
|
||||
if (this.isUsingItem) this.itemUsageTicks++
|
||||
})
|
||||
|
||||
// Initial held items setup
|
||||
this.updateHeldItem(false)
|
||||
this.updateHeldItem(true)
|
||||
}
|
||||
|
||||
// #region Movement and Physics State
|
||||
private updateState () {
|
||||
if (!bot.player?.entity || this.disableStateUpdates) return
|
||||
|
||||
const { velocity } = bot.player.entity
|
||||
const isOnGround = bot.entity.onGround
|
||||
const VELOCITY_THRESHOLD = 0.01
|
||||
const SPRINTING_VELOCITY = 0.15
|
||||
const OFF_GROUND_THRESHOLD = 0 // ms before switching to SNEAKING when off ground
|
||||
|
||||
const now = performance.now()
|
||||
const deltaTime = now - this.lastUpdateTime
|
||||
this.lastUpdateTime = now
|
||||
|
||||
this.lastVelocity = velocity
|
||||
|
||||
// Update time off ground
|
||||
if (isOnGround) {
|
||||
this.timeOffGround = 0
|
||||
} else {
|
||||
this.timeOffGround += deltaTime
|
||||
}
|
||||
|
||||
if (this.isSneaking() || this.isFlying() || (this.timeOffGround > OFF_GROUND_THRESHOLD)) {
|
||||
this.movementState = 'SNEAKING'
|
||||
} else if (Math.abs(velocity.x) > VELOCITY_THRESHOLD || Math.abs(velocity.z) > VELOCITY_THRESHOLD) {
|
||||
this.movementState = Math.abs(velocity.x) > SPRINTING_VELOCITY || Math.abs(velocity.z) > SPRINTING_VELOCITY
|
||||
? 'SPRINTING'
|
||||
: 'WALKING'
|
||||
} else {
|
||||
this.movementState = 'NOT_MOVING'
|
||||
}
|
||||
}
|
||||
|
||||
getMovementState (): MovementState {
|
||||
return this.movementState
|
||||
}
|
||||
|
||||
getVelocity (): Vec3 {
|
||||
return this.lastVelocity
|
||||
}
|
||||
|
||||
getEyeHeight (): number {
|
||||
return bot.controlState.sneak ? 1.27 : bot.entity?.['eyeHeight'] ?? 1.62
|
||||
}
|
||||
|
||||
isOnGround (): boolean {
|
||||
return bot?.entity?.onGround ?? true
|
||||
}
|
||||
|
||||
isSneaking (): boolean {
|
||||
return gameAdditionalState.isSneaking
|
||||
}
|
||||
|
||||
isFlying (): boolean {
|
||||
return gameAdditionalState.isFlying
|
||||
}
|
||||
|
||||
isSprinting (): boolean {
|
||||
return gameAdditionalState.isSprinting
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Held Item State
|
||||
private updateHeldItem (isLeftHand: boolean) {
|
||||
const newItem = isLeftHand ? bot.inventory.slots[45] : bot.heldItem
|
||||
if (!newItem) {
|
||||
if (isLeftHand) {
|
||||
this.offHandItem = undefined
|
||||
} else {
|
||||
this.heldItem = undefined
|
||||
}
|
||||
this.events.emit('heldItemChanged', undefined, isLeftHand)
|
||||
return
|
||||
}
|
||||
|
||||
const block = loadedData.blocksByName[newItem.name]
|
||||
const blockProperties = block ? new window.PrismarineBlock(block.id, 'void', newItem.metadata).getProperties() : {}
|
||||
const item: HandItemBlock = {
|
||||
name: newItem.name,
|
||||
properties: blockProperties,
|
||||
id: newItem.type,
|
||||
type: block ? 'block' : 'item',
|
||||
fullItem: newItem,
|
||||
}
|
||||
|
||||
if (isLeftHand) {
|
||||
this.offHandItem = item
|
||||
} else {
|
||||
this.heldItem = item
|
||||
}
|
||||
this.events.emit('heldItemChanged', item, isLeftHand)
|
||||
}
|
||||
|
||||
startUsingItem () {
|
||||
if (this.isUsingItem) return
|
||||
this.isUsingItem = true
|
||||
this.itemUsageTicks = 0
|
||||
}
|
||||
|
||||
stopUsingItem () {
|
||||
this.isUsingItem = false
|
||||
this.itemUsageTicks = 0
|
||||
}
|
||||
|
||||
getItemUsageTicks (): number {
|
||||
return this.itemUsageTicks
|
||||
}
|
||||
|
||||
getHeldItem (isLeftHand = false): HandItemBlock | undefined {
|
||||
return isLeftHand ? this.offHandItem : this.heldItem
|
||||
}
|
||||
|
||||
getItemSelector (specificProperties: ItemSpecificContextProperties, item?: import('prismarine-item').Item): ItemSelector['properties'] {
|
||||
return {
|
||||
...specificProperties,
|
||||
'minecraft:date': new Date(),
|
||||
// "minecraft:context_dimension": bot.entityp,
|
||||
'minecraft:time': bot.time.timeOfDay / 24_000,
|
||||
}
|
||||
}
|
||||
// #endregion
|
||||
}
|
||||
|
||||
export const playerState = PlayerStateManager.getInstance()
|
||||
window.playerState = playerState
|
||||
42
src/mineflayer/plugins/ping.ts
Normal file
42
src/mineflayer/plugins/ping.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { versionToNumber } from 'renderer/viewer/prepare/utils'
|
||||
|
||||
export default () => {
|
||||
let i = 0
|
||||
bot.pingProxy = async () => {
|
||||
const curI = ++i
|
||||
return new Promise(resolve => {
|
||||
//@ts-expect-error
|
||||
bot._client.socket._ws.send(`ping:${curI}`)
|
||||
const date = Date.now()
|
||||
const onPong = (received) => {
|
||||
if (received !== curI.toString()) return
|
||||
bot._client.socket.off('pong' as any, onPong)
|
||||
resolve(Date.now() - date)
|
||||
}
|
||||
bot._client.socket.on('pong' as any, onPong)
|
||||
})
|
||||
}
|
||||
|
||||
let pingId = 0
|
||||
bot.pingServer = async () => {
|
||||
if (versionToNumber(bot.version) < versionToNumber('1.20.2')) return bot.player.ping
|
||||
return new Promise<number>((resolve) => {
|
||||
const curId = pingId++
|
||||
bot._client.write('ping_request', { id: BigInt(curId) })
|
||||
const date = Date.now()
|
||||
const onPong = (data: { id: bigint }) => {
|
||||
if (BigInt(data.id) !== BigInt(curId)) return
|
||||
bot._client.off('ping_response' as any, onPong)
|
||||
resolve(Date.now() - date)
|
||||
}
|
||||
bot._client.on('ping_response' as any, onPong)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'mineflayer' {
|
||||
interface Bot {
|
||||
pingProxy: () => Promise<number>
|
||||
pingServer: () => Promise<number | undefined>
|
||||
}
|
||||
}
|
||||
|
|
@ -91,7 +91,9 @@ export const guiOptionsScheme: {
|
|||
unit: '',
|
||||
tooltip: 'Additional distance to keep the chunks loading before unloading them by marking them as too far',
|
||||
},
|
||||
handDisplay: {},
|
||||
renderEars: {
|
||||
tooltip: 'Enable rendering Deadmau5 ears for all players if their skin contains textures for it',
|
||||
},
|
||||
renderDebug: {
|
||||
values: [
|
||||
'advanced',
|
||||
|
|
@ -248,6 +250,12 @@ export const guiOptionsScheme: {
|
|||
['classic', 'Classic']
|
||||
],
|
||||
},
|
||||
showHand: {
|
||||
text: 'Show Hand',
|
||||
},
|
||||
viewBobbing: {
|
||||
text: 'View Bobbing',
|
||||
},
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
|
|
|
|||
|
|
@ -46,14 +46,17 @@ const defaultOptions = {
|
|||
unimplementedContainers: false,
|
||||
dayCycleAndLighting: true,
|
||||
loadPlayerSkins: true,
|
||||
renderEars: true,
|
||||
lowMemoryMode: false,
|
||||
starfieldRendering: true,
|
||||
enabledResourcepack: null as string | null,
|
||||
useVersionsTextures: 'latest',
|
||||
serverResourcePacks: 'prompt' as 'prompt' | 'always' | 'never',
|
||||
handDisplay: false,
|
||||
showHand: true,
|
||||
viewBobbing: true,
|
||||
packetsLoggerPreset: 'all' as 'all' | 'no-buffers',
|
||||
serversAutoVersionSelect: 'auto' as 'auto' | 'latest' | '1.20.4' | string,
|
||||
customChannels: false,
|
||||
|
||||
// antiAliasing: false,
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ export const parseServerAddress = (address: string | undefined, removeHttp = tru
|
|||
return { host: '', isWebSocket: false, serverIpFull: '' }
|
||||
}
|
||||
|
||||
if (/^ws:[^/]/.test(address)) address = address.replace('ws:', 'ws://')
|
||||
if (/^wss:[^/]/.test(address)) address = address.replace('wss:', 'wss://')
|
||||
const isWebSocket = address.startsWith('ws://') || address.startsWith('wss://')
|
||||
if (isWebSocket) {
|
||||
return { host: address, isWebSocket: true, serverIpFull: address }
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import Screen from './Screen'
|
|||
import Input from './Input'
|
||||
import Button from './Button'
|
||||
import SelectGameVersion from './SelectGameVersion'
|
||||
import { useIsSmallWidth, usePassesWindowDimensions } from './simpleHooks'
|
||||
import { usePassesScaledDimensions } from './UIProvider'
|
||||
|
||||
export interface BaseServerInfo {
|
||||
ip: string
|
||||
|
|
@ -35,13 +35,14 @@ interface Props {
|
|||
const ELEMENTS_WIDTH = 190
|
||||
|
||||
export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQs, onQsConnect, placeholders, accounts, versions, allowAutoConnect }: Props) => {
|
||||
const isSmallHeight = !usePassesWindowDimensions(null, 350)
|
||||
const isSmallHeight = !usePassesScaledDimensions(null, 350)
|
||||
const qsParamName = parseQs ? appQueryParams.name : undefined
|
||||
const qsParamIp = parseQs ? appQueryParams.ip : undefined
|
||||
const qsParamVersion = parseQs ? appQueryParams.version : undefined
|
||||
const qsParamProxy = parseQs ? appQueryParams.proxy : undefined
|
||||
const qsParamUsername = parseQs ? appQueryParams.username : undefined
|
||||
const qsParamLockConnect = parseQs ? appQueryParams.lockConnect : undefined
|
||||
const qsParamAutoConnect = parseQs ? appQueryParams.autoConnect : undefined
|
||||
|
||||
const parsedQsIp = parseServerAddress(qsParamIp)
|
||||
const parsedInitialIp = parseServerAddress(initialData?.ip)
|
||||
|
|
@ -54,7 +55,7 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
|
|||
const [usernameOverride, setUsernameOverride] = React.useState(initialData?.usernameOverride ?? qsParamUsername ?? '')
|
||||
const lockConnect = qsParamLockConnect === 'true'
|
||||
|
||||
const smallWidth = useIsSmallWidth()
|
||||
const smallWidth = !usePassesScaledDimensions(400)
|
||||
const initialAccount = initialData?.authenticatedAccountOverride
|
||||
const [accountIndex, setAccountIndex] = React.useState(initialAccount === true ? -2 : initialAccount ? (accounts?.includes(initialAccount) ? accounts.indexOf(initialAccount) : -2) : -1)
|
||||
|
||||
|
|
@ -121,11 +122,13 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
|
|||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (qsParamIp && qsParamVersion && allowAutoConnect) {
|
||||
if (qsParamAutoConnect && qsParamIp && qsParamVersion && allowAutoConnect) {
|
||||
onQsConnect?.(commonUseOptions)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const displayConnectButton = qsParamIp
|
||||
|
||||
return <Screen title={qsParamIp ? 'Connect to Server' : title} backdrop>
|
||||
<form
|
||||
style={{
|
||||
|
|
@ -139,9 +142,13 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
|
|||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
display: smallWidth ? 'flex' : 'grid',
|
||||
gap: 3,
|
||||
gridTemplateColumns: smallWidth ? '1fr' : '1fr 1fr'
|
||||
...(smallWidth ? {
|
||||
flexDirection: 'column',
|
||||
} : {
|
||||
gridTemplateColumns: '1fr 1fr'
|
||||
})
|
||||
}}
|
||||
>
|
||||
{!lockConnect && <>
|
||||
|
|
@ -219,17 +226,29 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
|
|||
{!lockConnect && <>
|
||||
<ButtonWrapper onClick={() => {
|
||||
onBack()
|
||||
}}>Cancel</ButtonWrapper>
|
||||
<ButtonWrapper type='submit'>Save</ButtonWrapper>
|
||||
}}>
|
||||
Cancel
|
||||
</ButtonWrapper>
|
||||
<ButtonWrapper type='submit'>
|
||||
{displayConnectButton ? 'Save' : <strong>Save</strong>}
|
||||
</ButtonWrapper>
|
||||
</>}
|
||||
{qsParamIp && <div style={{ gridColumn: smallWidth ? '' : 'span 2', display: 'flex', justifyContent: 'center' }}>
|
||||
<ButtonWrapper
|
||||
data-test-id='connect-qs'
|
||||
onClick={() => {
|
||||
onQsConnect?.(commonUseOptions)
|
||||
}}
|
||||
><strong>Connect</strong></ButtonWrapper>
|
||||
</div>}
|
||||
{displayConnectButton && (
|
||||
<div style={{
|
||||
gridColumn: smallWidth ? '' : 'span 2',
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<ButtonWrapper
|
||||
data-test-id='connect-qs'
|
||||
onClick={() => {
|
||||
onQsConnect?.(commonUseOptions)
|
||||
}}
|
||||
>
|
||||
<strong>Connect</strong>
|
||||
</ButtonWrapper>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Screen>
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export const resetAppStatusState = () => {
|
|||
export const lastConnectOptions = {
|
||||
value: null as ConnectOptions | null
|
||||
}
|
||||
globalThis.lastConnectOptions = lastConnectOptions
|
||||
|
||||
const saveReconnectOptions = (options: ConnectOptions) => {
|
||||
sessionStorage.setItem('reconnectOptions', JSON.stringify({
|
||||
|
|
@ -44,6 +45,13 @@ const saveReconnectOptions = (options: ConnectOptions) => {
|
|||
}))
|
||||
}
|
||||
|
||||
export const reconnectReload = () => {
|
||||
if (lastConnectOptions.value) {
|
||||
saveReconnectOptions(lastConnectOptions.value)
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const { isError, lastStatus, maybeRecoverable, status, hideDots, descriptionHint, loadingChunksData, loadingChunksDataPlayerChunk, minecraftJsonMessage, showReconnect } = useSnapshot(appStatusState)
|
||||
const { active: replayActive } = useSnapshot(packetsReplaceSessionState)
|
||||
|
|
@ -70,13 +78,6 @@ export default () => {
|
|||
}))
|
||||
}
|
||||
|
||||
const reconnectReload = () => {
|
||||
if (lastConnectOptions.value) {
|
||||
saveReconnectOptions(lastConnectOptions.value)
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController()
|
||||
window.addEventListener('keyup', (e) => {
|
||||
|
|
|
|||
|
|
@ -55,7 +55,8 @@ div.chat-wrapper {
|
|||
transform: none;
|
||||
top: 100%;
|
||||
padding-left: calc(env(safe-area-inset-left) / 2);
|
||||
margin-top: 20px;
|
||||
margin-top: 14px;
|
||||
margin-left: 20px;
|
||||
/* input height */
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import './Chat.css'
|
|||
import { isIos, reactKeyForMessage } from './utils'
|
||||
import Button from './Button'
|
||||
import { pixelartIcons } from './PixelartIcon'
|
||||
import { useScrollBehavior } from './hooks/useScrollBehavior'
|
||||
|
||||
export type Message = {
|
||||
parts: MessageFormatPart[],
|
||||
|
|
@ -69,6 +70,7 @@ export default ({
|
|||
placeholder
|
||||
}: Props) => {
|
||||
const sendHistoryRef = useRef(JSON.parse(window.sessionStorage.chatHistory || '[]'))
|
||||
const [isInputFocused, setIsInputFocused] = useState(false)
|
||||
|
||||
const [completePadText, setCompletePadText] = useState('')
|
||||
const completeRequestValue = useRef('')
|
||||
|
|
@ -77,10 +79,11 @@ export default ({
|
|||
|
||||
const chatInput = useRef<HTMLInputElement>(null!)
|
||||
const chatMessages = useRef<HTMLDivElement>(null)
|
||||
const openedChatWasAtBottom = useRef(false)
|
||||
const chatHistoryPos = useRef(sendHistoryRef.current.length)
|
||||
const inputCurrentlyEnteredValue = useRef('')
|
||||
|
||||
const { scrollToBottom } = useScrollBehavior(chatMessages, { messages, opened })
|
||||
|
||||
const setSendHistory = (newHistory: string[]) => {
|
||||
sendHistoryRef.current = newHistory
|
||||
window.sessionStorage.chatHistory = JSON.stringify(newHistory)
|
||||
|
|
@ -104,6 +107,11 @@ export default ({
|
|||
}, 0)
|
||||
}
|
||||
|
||||
const auxInputFocus = (fireKey: string) => {
|
||||
chatInput.current.focus()
|
||||
chatInput.current.dispatchEvent(new KeyboardEvent('keydown', { code: fireKey, bubbles: true }))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// todo focus input on any keypress except tab
|
||||
}, [])
|
||||
|
|
@ -120,16 +128,31 @@ export default ({
|
|||
if (!usingTouch) {
|
||||
chatInput.current.focus()
|
||||
}
|
||||
const unsubscribe = subscribe(chatInputValueGlobal, () => {
|
||||
|
||||
// Add keyboard event listener for letter keys and paste
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// Check if it's a single character key (works with any layout) without modifiers except shift
|
||||
const isSingleChar = e.key.length === 1 && !e.ctrlKey && !e.altKey && !e.metaKey
|
||||
// Check if it's paste command
|
||||
const isPaste = e.code === 'KeyV' && (e.ctrlKey || e.metaKey)
|
||||
|
||||
if ((isSingleChar || isPaste) && document.activeElement !== chatInput.current) {
|
||||
chatInput.current.focus()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
const unsubscribeValtio = subscribe(chatInputValueGlobal, () => {
|
||||
if (!chatInputValueGlobal.value) return
|
||||
updateInputValue(chatInputValueGlobal.value)
|
||||
chatInputValueGlobal.value = ''
|
||||
chatInput.current.focus()
|
||||
})
|
||||
return unsubscribe
|
||||
}
|
||||
if (!opened && chatMessages.current) {
|
||||
chatMessages.current.scrollTop = chatMessages.current.scrollHeight
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown)
|
||||
unsubscribeValtio()
|
||||
}
|
||||
}
|
||||
}, [opened])
|
||||
|
||||
|
|
@ -140,59 +163,6 @@ export default ({
|
|||
}
|
||||
}, [opened])
|
||||
|
||||
useEffect(() => {
|
||||
if ((!opened || (opened && openedChatWasAtBottom.current)) && chatMessages.current) {
|
||||
openedChatWasAtBottom.current = false
|
||||
// stay at bottom on messages changes
|
||||
chatMessages.current.scrollTop = chatMessages.current.scrollHeight
|
||||
}
|
||||
}, [messages])
|
||||
|
||||
useMemo(() => {
|
||||
if ((opened && chatMessages.current)) {
|
||||
const wasAtBottom = chatMessages.current.scrollTop === chatMessages.current.scrollHeight - chatMessages.current.clientHeight
|
||||
openedChatWasAtBottom.current = wasAtBottom
|
||||
// console.log(wasAtBottom, chatMessages.current.scrollTop, chatMessages.current.scrollHeight - chatMessages.current.clientHeight)
|
||||
}
|
||||
}, [messages])
|
||||
|
||||
const auxInputFocus = (fireKey: string) => {
|
||||
chatInput.current.focus()
|
||||
chatInput.current.dispatchEvent(new KeyboardEvent('keydown', { code: fireKey, bubbles: true }))
|
||||
}
|
||||
|
||||
const getDefaultCompleteValue = () => {
|
||||
const raw = chatInput.current.value
|
||||
return raw.slice(0, chatInput.current.selectionEnd ?? raw.length)
|
||||
}
|
||||
const getCompleteValue = (value = getDefaultCompleteValue()) => {
|
||||
const valueParts = value.split(' ')
|
||||
const lastLength = valueParts.at(-1)!.length
|
||||
const completeValue = lastLength ? value.slice(0, -lastLength) : value
|
||||
if (valueParts.length === 1 && value.startsWith('/')) return '/'
|
||||
return completeValue
|
||||
}
|
||||
|
||||
const fetchCompletions = async (implicit: boolean, inputValue = chatInput.current.value) => {
|
||||
const completeValue = getCompleteValue(inputValue)
|
||||
completeRequestValue.current = completeValue
|
||||
resetCompletionItems()
|
||||
const newItems = await fetchCompletionItems?.(implicit ? 'implicit' : 'explicit', completeValue, inputValue) ?? []
|
||||
if (completeValue !== completeRequestValue.current) return
|
||||
setCompletionItemsSource(newItems)
|
||||
updateFilteredCompleteItems(newItems)
|
||||
}
|
||||
|
||||
const updateFilteredCompleteItems = (sourceItems: string[]) => {
|
||||
const newCompleteItems = sourceItems.filter(item => {
|
||||
// this regex is imporatnt is it controls the word matching
|
||||
const compareableParts = item.split(/[[\]{},_:]/)
|
||||
const lastWord = chatInput.current.value.slice(0, chatInput.current.selectionEnd ?? chatInput.current.value.length).split(' ').at(-1)!
|
||||
return [item, ...compareableParts].some(compareablePart => compareablePart.startsWith(lastWord))
|
||||
})
|
||||
setCompletionItems(newCompleteItems)
|
||||
}
|
||||
|
||||
const onMainInputChange = () => {
|
||||
const completeValue = getCompleteValue()
|
||||
setCompletePadText(completeValue === '/' ? '' : completeValue)
|
||||
|
|
@ -212,6 +182,40 @@ export default ({
|
|||
// }
|
||||
}
|
||||
|
||||
const fetchCompletions = async (implicit: boolean, inputValue = chatInput.current.value) => {
|
||||
const completeValue = getCompleteValue(inputValue)
|
||||
completeRequestValue.current = completeValue
|
||||
resetCompletionItems()
|
||||
const newItems = await fetchCompletionItems?.(implicit ? 'implicit' : 'explicit', completeValue, inputValue) ?? []
|
||||
if (completeValue !== completeRequestValue.current) return
|
||||
setCompletionItemsSource(newItems)
|
||||
updateFilteredCompleteItems(newItems)
|
||||
}
|
||||
|
||||
const updateFilteredCompleteItems = (sourceItems: string[] | Array<{ match: string, toolip: string }>) => {
|
||||
const newCompleteItems = sourceItems
|
||||
.map(item => (typeof item === 'string' ? item : item.match))
|
||||
.filter(item => {
|
||||
// this regex is imporatnt is it controls the word matching
|
||||
const compareableParts = item.split(/[[\]{},_:]/)
|
||||
const lastWord = chatInput.current.value.slice(0, chatInput.current.selectionEnd ?? chatInput.current.value.length).split(' ').at(-1)!
|
||||
return [item, ...compareableParts].some(compareablePart => compareablePart.startsWith(lastWord))
|
||||
})
|
||||
setCompletionItems(newCompleteItems)
|
||||
}
|
||||
|
||||
const getDefaultCompleteValue = () => {
|
||||
const raw = chatInput.current.value
|
||||
return raw.slice(0, chatInput.current.selectionEnd ?? raw.length)
|
||||
}
|
||||
const getCompleteValue = (value = getDefaultCompleteValue()) => {
|
||||
const valueParts = value.split(' ')
|
||||
const lastLength = valueParts.at(-1)!.length
|
||||
const completeValue = lastLength ? value.slice(0, -lastLength) : value
|
||||
if (valueParts.length === 1 && value.startsWith('/')) return '/'
|
||||
return completeValue
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
|
|
@ -230,12 +234,20 @@ export default ({
|
|||
{/* close button */}
|
||||
{usingTouch && <Button icon={pixelartIcons.close} onClick={() => onClose?.()} />}
|
||||
<div className="chat-input">
|
||||
{completionItems?.length ? (
|
||||
{isInputFocused && completionItems?.length ? (
|
||||
<div className="chat-completions">
|
||||
<div className="chat-completions-pad-text">{completePadText}</div>
|
||||
<div className="chat-completions-items">
|
||||
{completionItems.map((item) => (
|
||||
<div key={item} onClick={() => acceptComplete(item)}>{item}</div>
|
||||
<div
|
||||
key={item}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault() // Prevent blur before click
|
||||
acceptComplete(item)
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -249,6 +261,8 @@ export default ({
|
|||
if (result !== false) {
|
||||
onClose?.()
|
||||
}
|
||||
// Always scroll to bottom after sending a message
|
||||
scrollToBottom()
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
@ -274,6 +288,8 @@ export default ({
|
|||
onChange={onMainInputChange}
|
||||
disabled={!!inputDisabled}
|
||||
placeholder={inputDisabled || placeholder}
|
||||
onFocus={() => setIsInputFocused(true)}
|
||||
onBlur={() => setIsInputFocused(false)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.code === 'ArrowUp') {
|
||||
if (chatHistoryPos.current === 0) return
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export default () => {
|
|||
const [blockL, setBlockL] = useState(0)
|
||||
const [biomeId, setBiomeId] = useState(0)
|
||||
const [day, setDay] = useState(0)
|
||||
const [entitiesCount, setEntitiesCount] = useState(0)
|
||||
const [entitiesCount, setEntitiesCount] = useState('0')
|
||||
const [dimension, setDimension] = useState('')
|
||||
const [cursorBlock, setCursorBlock] = useState<Block | null>(null)
|
||||
const minecraftYaw = useRef(0)
|
||||
|
|
@ -106,7 +106,7 @@ export default () => {
|
|||
setDimension(bot.game.dimension)
|
||||
setDay(bot.time.day)
|
||||
setCursorBlock(bot.blockAtCursor(5))
|
||||
setEntitiesCount(Object.values(bot.entities).length)
|
||||
setEntitiesCount(`${viewer.entities.entitiesRenderingCount} (${Object.values(bot.entities).length})`)
|
||||
}, 100)
|
||||
|
||||
// @ts-expect-error
|
||||
|
|
|
|||
|
|
@ -2,23 +2,21 @@ import { Vec3 } from 'vec3'
|
|||
import { useRef, useEffect, useState, CSSProperties, Dispatch, SetStateAction } from 'react'
|
||||
import { WorldWarp } from 'flying-squid/dist/lib/modules/warps'
|
||||
import { TransformWrapper, TransformComponent, ReactZoomPanPinchRef } from 'react-zoom-pan-pinch'
|
||||
import { MinimapDrawer, DrawerAdapter, ChunkInfo } from './MinimapDrawer'
|
||||
import { DrawerAdapter } from './MinimapDrawer'
|
||||
import Button from './Button'
|
||||
import Input from './Input'
|
||||
import './Fullmap.css'
|
||||
|
||||
|
||||
type FullmapProps = {
|
||||
toggleFullMap: () => void,
|
||||
adapter: DrawerAdapter,
|
||||
drawer: MinimapDrawer | null,
|
||||
canvasRef: any
|
||||
toggleFullMap?: () => void,
|
||||
}
|
||||
|
||||
export default ({ toggleFullMap, adapter }: FullmapProps) => {
|
||||
export default ({ adapter, toggleFullMap }: FullmapProps) => {
|
||||
const [grid, setGrid] = useState(() => new Set<string>())
|
||||
const zoomRef = useRef<ReactZoomPanPinchRef>(null)
|
||||
const redrawCell = useRef(false)
|
||||
const [redraw, setRedraw] = useState<Set<string> | null>(null)
|
||||
const [lastWarpPos, setLastWarpPos] = useState({ x: 0, y: 0, z: 0 })
|
||||
const stateRef = useRef({ scale: 1, positionX: 0, positionY: 0 })
|
||||
const cells = useRef({ columns: 0, rows: 0 })
|
||||
|
|
@ -73,7 +71,7 @@ export default ({ toggleFullMap, adapter }: FullmapProps) => {
|
|||
zIndex: '-1'
|
||||
}}
|
||||
onClick={toggleFullMap}
|
||||
/>
|
||||
> </div>
|
||||
: <Button
|
||||
icon="close-box"
|
||||
onClick={toggleFullMap}
|
||||
|
|
@ -129,7 +127,7 @@ export default ({ toggleFullMap, adapter }: FullmapProps) => {
|
|||
worldZ={playerChunkTop + y / 4 - offsetY}
|
||||
setIsWarpInfoOpened={setIsWarpInfoOpened}
|
||||
setLastWarpPos={setLastWarpPos}
|
||||
redraw={redrawCell.current}
|
||||
redraw={redraw}
|
||||
setInitWarp={setInitWarp}
|
||||
setWarpPreview={setWarpPreview}
|
||||
/>
|
||||
|
|
@ -156,9 +154,7 @@ export default ({ toggleFullMap, adapter }: FullmapProps) => {
|
|||
adapter={adapter}
|
||||
warpPos={lastWarpPos}
|
||||
setIsWarpInfoOpened={setIsWarpInfoOpened}
|
||||
afterWarpIsSet={() => {
|
||||
redrawCell.current = !redrawCell.current
|
||||
}}
|
||||
setRedraw={setRedraw}
|
||||
initWarp={initWarp}
|
||||
setInitWarp={setInitWarp}
|
||||
toggleFullMap={toggleFullMap}
|
||||
|
|
@ -179,16 +175,14 @@ const MapChunk = (
|
|||
worldZ: number,
|
||||
setIsWarpInfoOpened: (x: boolean) => void,
|
||||
setLastWarpPos: (obj: { x: number, y: number, z: number }) => void,
|
||||
redraw?: boolean
|
||||
redraw?: Set<string> | null
|
||||
setInitWarp?: (warp: WorldWarp | undefined) => void
|
||||
setWarpPreview?: (warpInfo) => void
|
||||
}
|
||||
) => {
|
||||
const containerRef = useRef(null)
|
||||
const drawerRef = useRef<MinimapDrawer | null>(null)
|
||||
const touchTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const [isCanvas, setIsCanvas] = useState(false)
|
||||
|
||||
const longPress = (e) => {
|
||||
touchTimer.current = setTimeout(() => {
|
||||
|
|
@ -202,8 +196,8 @@ const MapChunk = (
|
|||
}
|
||||
|
||||
const handleClick = (e: MouseEvent | TouchEvent) => {
|
||||
// console.log('click:', e)
|
||||
if (!drawerRef.current) return
|
||||
if (!adapter.mapDrawer) return
|
||||
console.log('click:', e)
|
||||
let clientX: number
|
||||
let clientY: number
|
||||
if ('buttons' in e && e.button === 2) {
|
||||
|
|
@ -217,9 +211,9 @@ const MapChunk = (
|
|||
const mapX = Math.floor(x + worldX)
|
||||
const mapZ = Math.floor(z + worldZ)
|
||||
const y = adapter.getHighestBlockY(mapX, mapZ)
|
||||
drawerRef.current.setWarpPosOnClick(new Vec3(mapX, y, mapZ))
|
||||
setLastWarpPos(drawerRef.current.lastWarpPos)
|
||||
const { lastWarpPos } = drawerRef.current
|
||||
adapter.mapDrawer.setWarpPosOnClick(new Vec3(mapX, y, mapZ))
|
||||
setLastWarpPos(adapter.mapDrawer.lastWarpPos)
|
||||
const { lastWarpPos } = adapter.mapDrawer
|
||||
const initWarp = adapter.warps.find(warp => Math.hypot(lastWarpPos.x - warp.x, lastWarpPos.z - warp.z) < 2)
|
||||
setInitWarp?.(initWarp)
|
||||
setIsWarpInfoOpened(true)
|
||||
|
|
@ -227,7 +221,7 @@ const MapChunk = (
|
|||
|
||||
const getXZ = (clientX: number, clientY: number) => {
|
||||
const rect = canvasRef.current!.getBoundingClientRect()
|
||||
const factor = scale * (drawerRef.current?.mapPixel ?? 1)
|
||||
const factor = scale * (adapter.mapDrawer.mapPixel ?? 1)
|
||||
const x = (clientX - rect.left) / factor
|
||||
const y = (clientY - rect.top) / factor
|
||||
return [x, y]
|
||||
|
|
@ -241,34 +235,18 @@ const MapChunk = (
|
|||
)
|
||||
}
|
||||
|
||||
const handleRedraw = (key?: string, chunk?: ChunkInfo) => {
|
||||
const handleRedraw = (key?: string) => {
|
||||
if (key !== `${worldX / 16},${worldZ / 16}`) return
|
||||
adapter.mapDrawer.canvas = canvasRef.current!
|
||||
adapter.mapDrawer.full = true
|
||||
// console.log('handle redraw:', key)
|
||||
// if (chunk) {
|
||||
// drawerRef.current?.chunksStore.set(key, chunk)
|
||||
// }
|
||||
if (!adapter.chunksStore.has(key)) {
|
||||
adapter.chunksStore.set(key, 'requested')
|
||||
void adapter.loadChunk(key)
|
||||
return
|
||||
}
|
||||
console.log('[mapChunk] update', key, `${worldX / 16},${worldZ / 16}`)
|
||||
const timeout = setTimeout(() => {
|
||||
const center = new Vec3(worldX + 8, 0, worldZ + 8)
|
||||
drawerRef.current!.lastBotPos = center
|
||||
drawerRef.current?.drawChunk(key)
|
||||
// drawerRef.current?.drawWarps(center)
|
||||
// drawerRef.current?.drawPlayerPos(center.x, center.z)
|
||||
if (canvasRef.current) void adapter.drawChunkOnCanvas(`${worldX / 16},${worldZ / 16}`, canvasRef.current)
|
||||
clearTimeout(timeout)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// if (canvasRef.current && !drawerRef.current) {
|
||||
// drawerRef.current = adapter.mapDrawer
|
||||
// } else if (canvasRef.current && drawerRef.current) {
|
||||
// }
|
||||
if (canvasRef.current) void adapter.drawChunkOnCanvas(`${worldX / 16},${worldZ / 16}`, canvasRef.current)
|
||||
}, [canvasRef.current])
|
||||
|
||||
|
|
@ -286,29 +264,15 @@ const MapChunk = (
|
|||
canvasRef.current?.removeEventListener('touchmove', cancel)
|
||||
canvasRef.current?.removeEventListener('mousemove', handleMouseMove)
|
||||
}
|
||||
}, [canvasRef.current, scale])
|
||||
}, [canvasRef.current])
|
||||
|
||||
useEffect(() => {
|
||||
// handleRedraw()
|
||||
}, [drawerRef.current, redraw])
|
||||
|
||||
useEffect(() => {
|
||||
const intersectionObserver = new IntersectionObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
setIsCanvas(true)
|
||||
}
|
||||
if (redraw) {
|
||||
for (const key of redraw) {
|
||||
handleRedraw(key)
|
||||
}
|
||||
})
|
||||
intersectionObserver.observe(containerRef.current!)
|
||||
|
||||
// adapter.on('chunkReady', handleRedraw)
|
||||
|
||||
return () => {
|
||||
intersectionObserver.disconnect()
|
||||
// adapter.off('chunkReady', handleRedraw)
|
||||
}
|
||||
}, [])
|
||||
}, [redraw])
|
||||
|
||||
return <div
|
||||
ref={containerRef}
|
||||
|
|
@ -334,7 +298,7 @@ const MapChunk = (
|
|||
}
|
||||
|
||||
const WarpInfo = (
|
||||
{ adapter, warpPos, setIsWarpInfoOpened, afterWarpIsSet, initWarp, toggleFullMap }:
|
||||
{ adapter, warpPos, setIsWarpInfoOpened, afterWarpIsSet, initWarp, toggleFullMap, setRedraw }:
|
||||
{
|
||||
adapter: DrawerAdapter,
|
||||
warpPos: { x: number, y: number, z: number },
|
||||
|
|
@ -342,7 +306,8 @@ const WarpInfo = (
|
|||
afterWarpIsSet?: () => void
|
||||
initWarp?: WorldWarp,
|
||||
setInitWarp?: React.Dispatch<React.SetStateAction<WorldWarp | undefined>>,
|
||||
toggleFullMap?: ({ command }: { command: string }) => void
|
||||
toggleFullMap?: () => void,
|
||||
setRedraw?: React.Dispatch<React.SetStateAction<Set<string> | null>>
|
||||
}
|
||||
) => {
|
||||
const [warp, setWarp] = useState<WorldWarp>(initWarp ?? {
|
||||
|
|
@ -365,14 +330,14 @@ const WarpInfo = (
|
|||
}
|
||||
|
||||
const updateChunk = () => {
|
||||
const redraw = new Set<string>()
|
||||
for (let i = -1; i < 2; i += 1) {
|
||||
for (let j = -1; j < 2; j += 1) {
|
||||
adapter.emit(
|
||||
'chunkReady',
|
||||
`${Math.floor(warp.x / 16) + j},${Math.floor(warp.z / 16) + i}`
|
||||
)
|
||||
redraw.add(`${Math.floor(warp.x / 16) + j},${Math.floor(warp.z / 16) + i}`)
|
||||
}
|
||||
}
|
||||
setRedraw?.(redraw)
|
||||
console.log('[warpInfo] update', redraw)
|
||||
}
|
||||
|
||||
const tpNow = () => {
|
||||
|
|
@ -380,7 +345,7 @@ const WarpInfo = (
|
|||
}
|
||||
|
||||
const quickTp = () => {
|
||||
toggleFullMap?.({ command: 'ui.toggleMap' })
|
||||
toggleFullMap?.()
|
||||
adapter.quickTp?.(warp.x, warp.z)
|
||||
}
|
||||
|
||||
|
|
@ -486,7 +451,8 @@ const WarpInfo = (
|
|||
}}
|
||||
>Cancel</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
adapter.setWarp({ ...warp })
|
||||
console.log(adapter.warps)
|
||||
setIsWarpInfoOpened(false)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ function InnerSearch () {
|
|||
margin: 'auto',
|
||||
zIndex: 11,
|
||||
width: 'min-content',
|
||||
transform: 'scale(1.5)'
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
|
|
|
|||
|
|
@ -12,16 +12,6 @@ export default ({ message, fallbackColor, className }: {
|
|||
}) => {
|
||||
const messageJson = useMemo(() => {
|
||||
if (!message) return null
|
||||
// const transformIfNbt = (x) => {
|
||||
// if (typeof x === 'object' && x?.type) return nbt.simplify(x) as Record<string, any>
|
||||
// // if (Array.isArray(x)) return x.map(transformIfNbt)
|
||||
// // if (typeof x === 'object') return Object.fromEntries(Object.entries(x).map(([k, v]) => [k, transformIfNbt(v)]))
|
||||
// return x
|
||||
// }
|
||||
// if (typeof message === 'object' && message.text?.text?.type) {
|
||||
// message.text.text = transformIfNbt(message.text.text)
|
||||
// message.text.extra = transformIfNbt(message.text.extra)
|
||||
// }
|
||||
try {
|
||||
const texts = formatMessage(typeof message === 'string' ? fromFormattedString(message) : message)
|
||||
return texts.map(text => {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from 'react'
|
|||
import './MineflayerPluginConsole.css'
|
||||
import { miscUiState } from '../globalState'
|
||||
import { useIsModalActive } from './utilsApp'
|
||||
import { useScrollBehavior } from './hooks/useScrollBehavior'
|
||||
|
||||
export type ConsoleMessage = {
|
||||
text: string
|
||||
|
|
@ -36,11 +37,14 @@ export default () => {
|
|||
const consoleInput = useRef<HTMLInputElement>(null!)
|
||||
const consoleMessages = useRef<HTMLDivElement>(null)
|
||||
|
||||
const { scrollToBottom } = useScrollBehavior(consoleMessages, { messages, opened })
|
||||
|
||||
// Add useEffect to focus input when opened
|
||||
useEffect(() => {
|
||||
if (consoleMessages.current) {
|
||||
consoleMessages.current.scrollTop = consoleMessages.current.scrollHeight
|
||||
if (opened && replEnabled) {
|
||||
consoleInput.current?.focus()
|
||||
}
|
||||
}, [messages])
|
||||
}, [opened, replEnabled])
|
||||
|
||||
const updateInputValue = (newValue: string) => {
|
||||
consoleInput.current.value = newValue
|
||||
|
|
@ -78,6 +82,8 @@ export default () => {
|
|||
if (code) {
|
||||
onExecute?.(code)
|
||||
updateInputValue('')
|
||||
// Scroll to bottom after sending command
|
||||
scrollToBottom()
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,50 @@
|
|||
import { proxy, useSnapshot } from 'valtio'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useEffect, useRef, Fragment } from 'react'
|
||||
import type { UIDefinition } from 'mcraft-fun-mineflayer/build/customChannel'
|
||||
import MessageFormattedString from './MessageFormattedString'
|
||||
import { useUiMotion } from './uiMotion'
|
||||
import PixelartIcon from './PixelartIcon'
|
||||
|
||||
export const mineflayerPluginHudState = proxy({
|
||||
ui: [] as Array<UIDefinition & { id: string }>,
|
||||
})
|
||||
|
||||
type TextPart = { type: 'text'; content: string } | { type: 'icon'; iconName: string }
|
||||
|
||||
const parseTextWithIcons = (text: string): TextPart[] => {
|
||||
const parts: TextPart[] = []
|
||||
let currentText = ''
|
||||
let i = 0
|
||||
|
||||
while (i < text.length) {
|
||||
if (text[i] === '{' && text.slice(i, i + 6) === '{icon:') {
|
||||
// If we have accumulated text before the icon, add it
|
||||
if (currentText) {
|
||||
parts.push({ type: 'text', content: currentText })
|
||||
currentText = ''
|
||||
}
|
||||
|
||||
// Find the end of the icon placeholder
|
||||
const endBrace = text.indexOf('}', i)
|
||||
if (endBrace !== -1) {
|
||||
const iconName = text.slice(i + 6, endBrace)
|
||||
parts.push({ type: 'icon', iconName })
|
||||
i = endBrace + 1
|
||||
continue
|
||||
}
|
||||
}
|
||||
currentText += text[i]
|
||||
i++
|
||||
}
|
||||
|
||||
// Add any remaining text
|
||||
if (currentText) {
|
||||
parts.push({ type: 'text', content: currentText })
|
||||
}
|
||||
|
||||
return parts
|
||||
}
|
||||
|
||||
const TextElement = ({ text, x, y, motion = true, formatted = true, css = '', onTab = false }: UIDefinition & { type: 'text' }) => {
|
||||
const motionRef = useRef<HTMLDivElement>(null)
|
||||
const innerRef = useRef<HTMLDivElement>(null)
|
||||
|
|
@ -21,6 +58,8 @@ const TextElement = ({ text, x, y, motion = true, formatted = true, css = '', on
|
|||
|
||||
if (onTab && !document.hidden) return null
|
||||
|
||||
const parts = parseTextWithIcons(text)
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={motionRef}
|
||||
|
|
@ -29,10 +68,21 @@ const TextElement = ({ text, x, y, motion = true, formatted = true, css = '', on
|
|||
left: x,
|
||||
top: y,
|
||||
transition: motion ? 'transform 0.1s ease-out' : 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '2px'
|
||||
}}
|
||||
>
|
||||
<div ref={innerRef}>
|
||||
{formatted ? <MessageFormattedString message={text} /> : text}
|
||||
{parts.map((part, index) => (
|
||||
<Fragment key={index}>
|
||||
{part.type === 'text' ? (
|
||||
formatted ? <MessageFormattedString message={part.content} /> : part.content
|
||||
) : (
|
||||
<PixelartIcon iconName={part.iconName} width={12} styles={{ display: 'inline-block' }} />
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -58,7 +108,7 @@ export default () => {
|
|||
const { ui } = useSnapshot(mineflayerPluginHudState)
|
||||
|
||||
return (
|
||||
<div style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none' }}>
|
||||
<div style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none' }} className='mineflayer-plugin-hud'>
|
||||
{ui.map((element, index) => {
|
||||
if (element.type === 'lil') return null // Handled elsewhere
|
||||
if (element.type === 'text') return <TextElement key={index} {...element} />
|
||||
|
|
|
|||
|
|
@ -1,74 +0,0 @@
|
|||
import { Vec3 } from 'vec3'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { WorldWarp } from 'flying-squid/dist/lib/modules/warps'
|
||||
import { TypedEventEmitter } from 'contro-max/build/typedEventEmitter'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import Minimap from './Minimap'
|
||||
import { DrawerAdapter, MapUpdates } from './MinimapDrawer'
|
||||
|
||||
const meta: Meta<typeof Minimap> = {
|
||||
component: Minimap,
|
||||
decorators: [
|
||||
(Story, context) => {
|
||||
|
||||
useEffect(() => {
|
||||
console.log('map updated')
|
||||
adapter.emit('updateMap')
|
||||
|
||||
}, [context.args['fullMap']])
|
||||
|
||||
return <div> <Story /> </div>
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof Minimap>
|
||||
|
||||
|
||||
class DrawerAdapterImpl extends TypedEventEmitter<MapUpdates> {
|
||||
playerPosition: Vec3
|
||||
yaw: number
|
||||
warps: WorldWarp[]
|
||||
chunksStore: any = {}
|
||||
full: boolean
|
||||
|
||||
constructor (pos?: Vec3, warps?: WorldWarp[]) {
|
||||
super()
|
||||
this.playerPosition = pos ?? new Vec3(0, 0, 0)
|
||||
this.warps = warps ?? [] as WorldWarp[]
|
||||
}
|
||||
|
||||
async getHighestBlockColor (x: number, z: number) {
|
||||
console.log('got color')
|
||||
return 'green'
|
||||
}
|
||||
|
||||
getHighestBlockY (x: number, z: number) {
|
||||
return 0
|
||||
}
|
||||
|
||||
setWarp (warp: WorldWarp, remove?: boolean): void {
|
||||
const index = this.warps.findIndex(w => w.name === warp.name)
|
||||
if (index === -1) {
|
||||
this.warps.push(warp)
|
||||
} else {
|
||||
this.warps[index] = warp
|
||||
}
|
||||
this.emit('updateWarps')
|
||||
}
|
||||
|
||||
clearChunksStore (x: number, z: number) { }
|
||||
|
||||
async loadChunk (key: string) {}
|
||||
}
|
||||
|
||||
const adapter = new DrawerAdapterImpl() as any
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
adapter,
|
||||
fullMap: false
|
||||
},
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { useRef, useEffect, useState } from 'react'
|
||||
import { MinimapDrawer, DrawerAdapter, ChunkInfo } from './MinimapDrawer'
|
||||
import { miscUiState } from '../globalState'
|
||||
import { DrawerAdapter } from './MinimapDrawer'
|
||||
import Fullmap from './Fullmap'
|
||||
|
||||
|
||||
|
|
@ -13,36 +14,31 @@ export default (
|
|||
showFullmap: string,
|
||||
singleplayer: boolean,
|
||||
fullMap?: boolean,
|
||||
toggleFullMap?: ({ command }: { command: string }) => void
|
||||
toggleFullMap?: () => void
|
||||
displayMode?: DisplayMode
|
||||
}
|
||||
) => {
|
||||
const full = useRef(false)
|
||||
const canvasTick = useRef(0)
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const warpsAndPartsCanvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const playerPosCanvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const warpsDrawerRef = useRef<MinimapDrawer | null>(null)
|
||||
const drawerRef = useRef<MinimapDrawer | null>(null)
|
||||
const playerPosDrawerRef = useRef<MinimapDrawer | null>(null)
|
||||
const [position, setPosition] = useState({ x: 0, y: 0, z: 0 })
|
||||
|
||||
const updateMap = () => {
|
||||
setPosition({ x: adapter.playerPosition.x, y: adapter.playerPosition.y, z: adapter.playerPosition.z })
|
||||
if (drawerRef.current) {
|
||||
if (adapter.mapDrawer) {
|
||||
if (!full.current) {
|
||||
rotateMap()
|
||||
drawerRef.current.draw(adapter.playerPosition)
|
||||
drawerRef.current.drawPlayerPos()
|
||||
drawerRef.current.drawWarps()
|
||||
adapter.mapDrawer.draw(adapter.playerPosition)
|
||||
adapter.mapDrawer.drawPlayerPos()
|
||||
adapter.mapDrawer.drawWarps()
|
||||
}
|
||||
if (canvasTick.current % 300 === 0 && !fullMap) {
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(() => {
|
||||
drawerRef.current?.clearChunksStore()
|
||||
adapter.mapDrawer?.clearChunksStore()
|
||||
})
|
||||
} else {
|
||||
drawerRef.current.clearChunksStore()
|
||||
adapter.mapDrawer.clearChunksStore()
|
||||
}
|
||||
canvasTick.current = 0
|
||||
}
|
||||
|
|
@ -53,42 +49,17 @@ export default (
|
|||
const updateWarps = () => { }
|
||||
|
||||
const rotateMap = () => {
|
||||
if (!drawerRef.current) return
|
||||
drawerRef.current.canvas.style.transform = `rotate(${adapter.yaw}rad)`
|
||||
if (!warpsDrawerRef.current) return
|
||||
warpsDrawerRef.current.canvas.style.transform = `rotate(${adapter.yaw}rad)`
|
||||
}
|
||||
|
||||
const updateChunkOnMap = (key: string, chunk: ChunkInfo) => {
|
||||
adapter.chunksStore.set(key, chunk)
|
||||
if (!adapter.mapDrawer) return
|
||||
adapter.mapDrawer.canvas.style.transform = `rotate(${adapter.yaw}rad)`
|
||||
adapter.mapDrawer.yaw = adapter.yaw
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (canvasRef.current && !drawerRef.current) {
|
||||
drawerRef.current = adapter.mapDrawer
|
||||
drawerRef.current.canvas = canvasRef.current
|
||||
// drawerRef.current.adapter.on('chunkReady', updateChunkOnMap)
|
||||
} else if (canvasRef.current && drawerRef.current) {
|
||||
drawerRef.current.canvas = canvasRef.current
|
||||
if (canvasRef.current && adapter.mapDrawer && !miscUiState.displayFullmap) {
|
||||
adapter.mapDrawer.canvas = canvasRef.current
|
||||
adapter.mapDrawer.full = false
|
||||
}
|
||||
|
||||
}, [canvasRef.current])
|
||||
|
||||
// useEffect(() => {
|
||||
// if (warpsAndPartsCanvasRef.current && !warpsDrawerRef.current) {
|
||||
// warpsDrawerRef.current = new MinimapDrawer(warpsAndPartsCanvasRef.current, adapter)
|
||||
// } else if (warpsAndPartsCanvasRef.current && warpsDrawerRef.current) {
|
||||
// warpsDrawerRef.current.canvas = warpsAndPartsCanvasRef.current
|
||||
// }
|
||||
// }, [warpsAndPartsCanvasRef.current])
|
||||
|
||||
// useEffect(() => {
|
||||
// if (playerPosCanvasRef.current && !playerPosDrawerRef.current) {
|
||||
// playerPosDrawerRef.current = new MinimapDrawer(playerPosCanvasRef.current, adapter)
|
||||
// } else if (playerPosCanvasRef.current && playerPosDrawerRef.current) {
|
||||
// playerPosDrawerRef.current.canvas = playerPosCanvasRef.current
|
||||
// }
|
||||
// }, [playerPosCanvasRef.current])
|
||||
}, [canvasRef.current, miscUiState.displayFullmap])
|
||||
|
||||
useEffect(() => {
|
||||
adapter.on('updateMap', updateMap)
|
||||
|
|
@ -100,24 +71,12 @@ export default (
|
|||
}
|
||||
}, [adapter])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// if (drawerRef.current) drawerRef.current.adapter.off('chunkReady', updateChunkOnMap)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const displayFullmap = fullMap && displayMode !== 'minimapOnly' && (showFullmap === 'singleplayer' && singleplayer || showFullmap === 'always')
|
||||
const displayMini = displayMode !== 'fullmapOnly' && (showMinimap === 'singleplayer' && singleplayer || showMinimap === 'always')
|
||||
return displayFullmap
|
||||
return fullMap && displayMode !== 'minimapOnly' && (showFullmap === 'singleplayer' && singleplayer || showFullmap === 'always')
|
||||
? <Fullmap
|
||||
toggleFullMap={() => {
|
||||
toggleFullMap?.({ command: 'ui.toggleMap' })
|
||||
}}
|
||||
toggleFullMap={toggleFullMap}
|
||||
adapter={adapter}
|
||||
drawer={drawerRef.current}
|
||||
canvasRef={canvasRef}
|
||||
/>
|
||||
: displayMini
|
||||
: displayMode !== 'fullmapOnly' && (showMinimap === 'singleplayer' && singleplayer || showMinimap === 'always')
|
||||
? <div
|
||||
className='minimap'
|
||||
style={{
|
||||
|
|
@ -128,7 +87,7 @@ export default (
|
|||
textAlign: 'center',
|
||||
}}
|
||||
onClick={() => {
|
||||
toggleFullMap?.({ command: 'ui.toggleMap' })
|
||||
toggleFullMap?.()
|
||||
}}
|
||||
>
|
||||
<canvas
|
||||
|
|
@ -141,28 +100,6 @@ export default (
|
|||
height={80}
|
||||
ref={canvasRef}
|
||||
/>
|
||||
<canvas
|
||||
style={{
|
||||
transition: '0.5s',
|
||||
transitionTimingFunction: 'ease-out',
|
||||
position: 'absolute',
|
||||
left: '0px'
|
||||
}}
|
||||
width={80}
|
||||
height={80}
|
||||
ref={warpsAndPartsCanvasRef}
|
||||
/>
|
||||
<canvas
|
||||
style={{
|
||||
transition: '0.5s',
|
||||
transitionTimingFunction: 'ease-out',
|
||||
position: 'absolute',
|
||||
left: '0px'
|
||||
}}
|
||||
width={80}
|
||||
height={80}
|
||||
ref={playerPosCanvasRef}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '0.5em',
|
||||
|
|
|
|||
|
|
@ -43,12 +43,20 @@ export class MinimapDrawer {
|
|||
lastWarpPos: Vec3
|
||||
mapPixel: number
|
||||
yaw: number
|
||||
chunksStore = new Map<string, undefined | null | 'requested' | ChunkInfo >()
|
||||
loadingChunksQueue: undefined | Set<string>
|
||||
warps: WorldWarp[]
|
||||
loadChunk: undefined | ((key: string) => Promise<void>)
|
||||
_full = false
|
||||
|
||||
constructor (
|
||||
public loadChunk: undefined | ((key: string) => Promise<void>),
|
||||
public warps: WorldWarp[],
|
||||
public loadingChunksQueue: undefined | Set<string>,
|
||||
public chunksStore: Map<string, undefined | null | 'requested' | ChunkInfo >
|
||||
) {
|
||||
this.loadChunk = loadChunk
|
||||
this.warps = warps
|
||||
this.loadingChunksQueue = loadingChunksQueue
|
||||
this.chunksStore = chunksStore
|
||||
}
|
||||
|
||||
setMapPixel () {
|
||||
if (this.full) {
|
||||
this.radius = Math.floor(Math.min(this.canvas.width, this.canvas.height) / 2)
|
||||
|
|
@ -101,6 +109,7 @@ export class MinimapDrawer {
|
|||
if (!this.chunksStore.has(key) && !this.loadingChunksQueue?.has(key)) {
|
||||
void this.loadChunk?.(key)
|
||||
}
|
||||
// case when chunk is not present is handled in drawChunk
|
||||
this.drawChunk(key)
|
||||
}
|
||||
if (!this.full) this.drawPartsOfWorld()
|
||||
|
|
@ -131,6 +140,7 @@ export class MinimapDrawer {
|
|||
const chunkCanvasX = Math.floor((chunkWorldX - this.lastBotPos.x) * this.mapPixel + this.canvasWidthCenterX)
|
||||
const chunkCanvasY = Math.floor((chunkWorldZ - this.lastBotPos.z) * this.mapPixel + this.canvasWidthCenterY)
|
||||
const chunk = chunkInfo ?? this.chunksStore.get(key)
|
||||
// if chunk is not ready then draw waiting color (grey) or none (half transparent black)
|
||||
if (typeof chunk !== 'object') {
|
||||
const chunkSize = this.mapPixel * 16
|
||||
this.ctx.fillStyle = chunk === 'requested' ? 'rgb(200, 200, 200)' : 'rgba(0, 0, 0, 0.5)'
|
||||
|
|
@ -149,10 +159,10 @@ export class MinimapDrawer {
|
|||
}
|
||||
|
||||
drawPixel (pixelX: number, pixelY: number, color: string) {
|
||||
// if (!this.full && Math.hypot(pixelX - this.canvasWidthCenterX, pixelY - this.canvasWidthCenterY) > this.radius) {
|
||||
// this.ctx.clearRect(pixelX, pixelY, this.mapPixel, this.mapPixel)
|
||||
// return
|
||||
// }
|
||||
if (!this.full && Math.hypot(pixelX - this.canvasWidthCenterX, pixelY - this.canvasWidthCenterY) > this.radius) {
|
||||
this.ctx.clearRect(pixelX, pixelY, this.mapPixel, this.mapPixel)
|
||||
return
|
||||
}
|
||||
this.ctx.fillStyle = color
|
||||
this.ctx.fillRect(
|
||||
pixelX,
|
||||
|
|
@ -177,15 +187,13 @@ export class MinimapDrawer {
|
|||
|
||||
drawWarps (centerPos?: Vec3) {
|
||||
for (const warp of this.warps) {
|
||||
// if (!full) {
|
||||
// const distance = this.getDistance(
|
||||
// centerPos?.x ?? this.adapter.playerPosition.x,
|
||||
// centerPos?.z ?? this.adapter.playerPosition.z,
|
||||
// warp.x,
|
||||
// warp.z
|
||||
// )
|
||||
// if (distance > this.mapSize) continue
|
||||
// }
|
||||
if (!this.full) {
|
||||
const distance = Math.hypot(
|
||||
centerPos?.x ?? this.lastBotPos.x - warp.x,
|
||||
centerPos?.z ?? this.lastBotPos.z - warp.z
|
||||
)
|
||||
if (distance > this.mapSize) continue
|
||||
}
|
||||
const offset = this.full ? 0 : this.radius * 0.1
|
||||
const z = Math.floor(
|
||||
(this.mapSize / 2 - (centerPos?.z ?? this.lastBotPos.z) + warp.z) * this.mapPixel
|
||||
|
|
@ -288,8 +296,8 @@ export class MinimapDrawer {
|
|||
drawPlayerPos (canvasWorldCenterX?: number, canvasWorldCenterZ?: number, disableTurn?: boolean) {
|
||||
this.ctx.setTransform(1, 0, 0, 1, 0, 0)
|
||||
|
||||
const x = (this.lastBotPos.x - (canvasWorldCenterX ?? this.lastBotPos.x)) * this.mapPixel
|
||||
const z = (this.lastBotPos.z - (canvasWorldCenterZ ?? this.lastBotPos.z)) * this.mapPixel
|
||||
const x = (this.lastBotPos.x - (canvasWorldCenterX ?? this.lastBotPos.x)) * this.mapPixel - (this.full ? 30 : 0)
|
||||
const z = (this.lastBotPos.z - (canvasWorldCenterZ ?? this.lastBotPos.z)) * this.mapPixel - (this.full ? 30 : 0)
|
||||
const center = this.mapSize / 2 * this.mapPixel + (this.full ? 0 : this.radius * 0.1)
|
||||
this.ctx.translate(center + x, center + z)
|
||||
if (!disableTurn) this.ctx.rotate(-this.yaw)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { versions } from 'minecraft-data'
|
||||
import { useEffect } from 'react'
|
||||
import { simplify } from 'prismarine-nbt'
|
||||
import RegionFile from 'prismarine-provider-anvil/src/region'
|
||||
import { Vec3 } from 'vec3'
|
||||
|
|
@ -15,17 +14,13 @@ import { useSnapshot } from 'valtio'
|
|||
import BlockData from '../../renderer/viewer/lib/moreBlockDataGenerated.json'
|
||||
import preflatMap from '../preflatMap.json'
|
||||
import { contro } from '../controls'
|
||||
import { gameAdditionalState, showModal, hideModal, miscUiState, activeModalStack } from '../globalState'
|
||||
import { gameAdditionalState, miscUiState } from '../globalState'
|
||||
import { options } from '../optionsStorage'
|
||||
import Minimap, { DisplayMode } from './Minimap'
|
||||
import { ChunkInfo, DrawerAdapter, MapUpdates, MinimapDrawer } from './MinimapDrawer'
|
||||
import { useIsModalActive } from './utilsApp'
|
||||
import { lastConnectOptions } from './AppStatusProvider'
|
||||
|
||||
const getBlockKey = (x: number, z: number) => {
|
||||
return `${x},${z}`
|
||||
}
|
||||
|
||||
const findHeightMap = (obj: PCChunk): number[] | undefined => {
|
||||
function search (obj: any): any | undefined {
|
||||
for (const key in obj) {
|
||||
|
|
@ -43,49 +38,45 @@ const findHeightMap = (obj: PCChunk): number[] | undefined => {
|
|||
export class DrawerAdapterImpl extends TypedEventEmitter<MapUpdates> implements DrawerAdapter {
|
||||
playerPosition: Vec3
|
||||
yaw: number
|
||||
mapDrawer = new MinimapDrawer()
|
||||
warps: WorldWarp[]
|
||||
world: string
|
||||
warps: WorldWarp[] = gameAdditionalState.warps
|
||||
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)
|
||||
currChunk: PCChunk | undefined
|
||||
currChunkPos: { x: number, z: number } = { x: 0, z: 0 }
|
||||
isOldVersion: boolean
|
||||
blockData: any
|
||||
blockData: Map<string | string[], string>
|
||||
heightMap: Record<string, number> = {}
|
||||
regions = new Map<string, RegionFile>()
|
||||
chunksHeightmaps: Record<string, any> = {}
|
||||
loadChunk: (key: string) => Promise<void>
|
||||
loadChunkFullmap: ((key: string) => Promise<ChunkInfo | null | undefined>) | undefined
|
||||
_full: boolean
|
||||
loadChunkFullmap: (key: string) => Promise<ChunkInfo | null | undefined>
|
||||
_full = false
|
||||
isBuiltinHeightmapAvailable = false
|
||||
|
||||
constructor (pos?: Vec3) {
|
||||
super()
|
||||
this.full = false
|
||||
this.playerPosition = pos ?? new Vec3(0, 0, 0)
|
||||
this.warps = gameAdditionalState.warps
|
||||
this.mapDrawer.warps = this.warps
|
||||
this.mapDrawer.loadChunk = this.loadChunk
|
||||
this.mapDrawer.loadingChunksQueue = this.loadingChunksQueue
|
||||
this.mapDrawer.chunksStore = this.chunksStore
|
||||
|
||||
// check if should use heightmap
|
||||
// check if should use heightmap.
|
||||
// As there is no simple way to check if heightmap is present in region file, making an attempt to load one
|
||||
if (localServer) {
|
||||
const chunkX = Math.floor(this.playerPosition.x / 16)
|
||||
const chunkZ = Math.floor(this.playerPosition.z / 16)
|
||||
const regionX = Math.floor(chunkX / 32)
|
||||
const regionZ = Math.floor(chunkZ / 32)
|
||||
const regionKey = `${regionX},${regionZ}`
|
||||
const worldFolder = this.getSingleplayerRootPath()
|
||||
if (worldFolder && options.minimapOptimizations) {
|
||||
const { worldFolder } = localServer.options
|
||||
if (worldFolder) {
|
||||
const path = `${worldFolder}/region/r.${regionX}.${regionZ}.mca`
|
||||
const region = new RegionFile(path)
|
||||
void region.initialize()
|
||||
this.regions.set(regionKey, region)
|
||||
const readX = chunkX % 32
|
||||
const readZ = chunkZ % 32
|
||||
void this.regions.get(regionKey)!.read(readX, readZ).then((rawChunk) => {
|
||||
const readX = chunkX % 32 < 0 ? 32 + chunkX % 32 : chunkX % 32
|
||||
const readZ = chunkZ % 32 < 0 ? 32 + chunkZ % 32 : chunkZ % 32
|
||||
console.log('heightmap check begun', readX, readZ)
|
||||
void this.regions.get(regionKey)?.read(readX, readZ)?.then((rawChunk) => {
|
||||
const chunk = simplify(rawChunk as any)
|
||||
const heightmap = findHeightMap(chunk)
|
||||
if (heightmap) {
|
||||
|
|
@ -95,44 +86,35 @@ export class DrawerAdapterImpl extends TypedEventEmitter<MapUpdates> implements
|
|||
} else {
|
||||
this.isBuiltinHeightmapAvailable = false
|
||||
this.loadChunkFullmap = this.loadChunkNoRegion
|
||||
console.log('[minimap] not using heightmap')
|
||||
console.log('dont use heightmap')
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error(err)
|
||||
this.isBuiltinHeightmapAvailable = false
|
||||
this.loadChunkFullmap = this.loadChunkFromViewer
|
||||
})
|
||||
} else {
|
||||
this.isBuiltinHeightmapAvailable = false
|
||||
this.loadChunkFullmap = this.loadChunkFromViewer
|
||||
this.loadChunkFullmap = this.loadChunkNoRegion
|
||||
console.log('dont use heightmap')
|
||||
}
|
||||
} else {
|
||||
this.isBuiltinHeightmapAvailable = false
|
||||
this.loadChunkFullmap = this.loadChunkFromViewer
|
||||
}
|
||||
// if (localServer) {
|
||||
// this.overwriteWarps(localServer.warps)
|
||||
// this.on('cellReady', (key: string) => {
|
||||
// if (this.loadingChunksQueue.size === 0) return
|
||||
// const [x, z] = this.loadingChunksQueue.values().next().value.split(',').map(Number)
|
||||
// this.loadChunk(x, z)
|
||||
// this.loadingChunksQueue.delete(`${x},${z}`)
|
||||
// })
|
||||
// } else {
|
||||
// const storageWarps = localStorage.getItem(`warps: ${loadedGameState.username} ${loadedGameState.serverIp ?? ''}`)
|
||||
// this.overwriteWarps(JSON.parse(storageWarps ?? '[]'))
|
||||
// }
|
||||
if (localServer) {
|
||||
this.overwriteWarps(localServer.warps)
|
||||
} else {
|
||||
const storageWarps = localStorage.getItem(`warps: ${lastConnectOptions.value?.server ?? 'server'} ${lastConnectOptions.value?.username ?? 'username'}`)
|
||||
this.overwriteWarps(JSON.parse(storageWarps ?? '[]'))
|
||||
}
|
||||
this.isOldVersion = versionToNumber(bot.version) < versionToNumber('1.13')
|
||||
this.blockData = {}
|
||||
this.blockData = new Map<string, string>()
|
||||
for (const blockKey of Object.keys(BlockData.colors)) {
|
||||
const renamedKey = getRenamedData('blocks', blockKey, '1.20.2', bot.version)
|
||||
this.blockData[renamedKey as string] = BlockData.colors[blockKey]
|
||||
this.blockData.set(renamedKey, BlockData.colors[blockKey])
|
||||
}
|
||||
|
||||
viewer.world?.renderUpdateEmitter.on('chunkFinished', (key) => {
|
||||
if (!this.loadingChunksQueue.has(key)) return
|
||||
void this.loadChunk(key)
|
||||
this.loadingChunksQueue.delete(key)
|
||||
void this.loadChunk(key)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -141,9 +123,11 @@ export class DrawerAdapterImpl extends TypedEventEmitter<MapUpdates> implements
|
|||
}
|
||||
|
||||
set full (full: boolean) {
|
||||
console.log('this is minimap')
|
||||
this.loadChunk = this.loadChunkMinimap
|
||||
this.mapDrawer.loadChunk = this.loadChunk
|
||||
if (!full) {
|
||||
console.log('this is minimap')
|
||||
this.loadChunk = this.loadChunkMinimap
|
||||
this.mapDrawer.loadChunk = this.loadChunk
|
||||
}
|
||||
this._full = full
|
||||
}
|
||||
|
||||
|
|
@ -157,7 +141,7 @@ export class DrawerAdapterImpl extends TypedEventEmitter<MapUpdates> implements
|
|||
setWarp (warp: WorldWarp, remove?: boolean): void {
|
||||
this.world = bot.game.dimension
|
||||
const index = this.warps.findIndex(w => w.name === warp.name)
|
||||
if (index === -1) {
|
||||
if (!remove && index === -1) {
|
||||
this.warps.push(warp)
|
||||
} else if (remove && index !== -1) {
|
||||
this.warps.splice(index, 1)
|
||||
|
|
@ -207,29 +191,29 @@ export class DrawerAdapterImpl extends TypedEventEmitter<MapUpdates> implements
|
|||
if (viewer.world.finishedChunks[`${chunkWorldX},${chunkWorldZ}`]) {
|
||||
const heightmap = new Uint8Array(256)
|
||||
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
|
||||
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 = viewer.world.highestBlocks.get(`${blockX},${blockZ}`)
|
||||
const block = bot.world.getBlock(new Vec3(blockX, hBlock?.y ?? 0, blockZ))
|
||||
// const block = Block.fromStateId(hBlock?.stateId ?? -1, hBlock?.biomeId ?? -1)
|
||||
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
|
||||
// blocks which are not set are shown as half transparent
|
||||
if (!block || !hBlock) {
|
||||
console.warn(`[loadChunk] ${chunkX}, ${chunkZ}, ${chunkWorldX + x}, ${chunkWorldZ + z}`)
|
||||
heightmap[index] = 0
|
||||
colors[index] = 'rgba(0, 0, 0, 0.5)'
|
||||
continue
|
||||
}
|
||||
heightmap[index] = hBlock.y
|
||||
let color: string
|
||||
if (this.isOldVersion) {
|
||||
color = BlockData.colors[preflatMap.blocks[`${block.type}:${block.metadata}`]?.replaceAll(/\[.*?]/g, '')]
|
||||
?? 'rgb(0, 0, 255)'
|
||||
} else {
|
||||
color = this.blockData[block.name] ?? 'rgb(0, 255, 0)'
|
||||
}
|
||||
colors[index] = color
|
||||
heightmap[index] = block.position.y
|
||||
colors[index] = this.setColor(block)
|
||||
}
|
||||
}
|
||||
const chunk = { heightmap, colors }
|
||||
|
|
@ -255,15 +239,19 @@ export class DrawerAdapterImpl extends TypedEventEmitter<MapUpdates> implements
|
|||
const blockX = chunkWorldX + x
|
||||
const blockZ = chunkWorldZ + z
|
||||
const blockY = this.getHighestBlockY(blockX, blockZ, chunkInfo)
|
||||
const block = chunkInfo.getBlock(new Vec3(blockX & 15, blockY, blockZ & 15))
|
||||
const blockPos = new Vec3(blockX & 15, blockY, blockZ & 15)
|
||||
let block = chunkInfo.getBlock(blockPos)
|
||||
while (block?.name.includes('air')) {
|
||||
blockPos.y -= 1
|
||||
block = chunkInfo.getBlock(blockPos)
|
||||
}
|
||||
if (!block) {
|
||||
console.warn(`[cannot get the block] ${chunkX}, ${chunkZ}, ${chunkWorldX + x}, ${chunkWorldZ + z}`)
|
||||
return null
|
||||
}
|
||||
const index = z * 16 + x
|
||||
heightmap[index] = blockY
|
||||
const color = this.isOldVersion ? BlockData.colors[preflatMap.blocks[`${block.type}:${block.metadata}`]?.replaceAll(/\[.*?]/g, '')] ?? 'rgb(0, 0, 255)' : this.blockData[block.name] ?? 'rgb(0, 255, 0)'
|
||||
colors[index] = color
|
||||
heightmap[index] = blockPos.y
|
||||
colors[index] = this.setColor(block)
|
||||
}
|
||||
}
|
||||
const chunk: ChunkInfo = { heightmap, colors }
|
||||
|
|
@ -288,13 +276,17 @@ export class DrawerAdapterImpl extends TypedEventEmitter<MapUpdates> implements
|
|||
heightmap[index] -= 1
|
||||
if (heightmap[index] < 0) heightmap[index] = 0
|
||||
const blockY = heightmap[index]
|
||||
const block = chunkInfo.getBlock(new Vec3(blockX & 15, blockY, blockZ & 15))
|
||||
const blockPos = new Vec3(blockX & 15, blockY, blockZ & 15)
|
||||
let block = chunkInfo.getBlock(blockPos)
|
||||
while (block?.name.includes('air')) {
|
||||
blockPos.y -= 1
|
||||
block = chunkInfo.getBlock(blockPos)
|
||||
}
|
||||
if (!block) {
|
||||
console.warn(`[cannot get the block] ${chunkX}, ${chunkZ}, ${chunkWorldX + x}, ${chunkWorldZ + z}`)
|
||||
return null
|
||||
}
|
||||
const color = this.isOldVersion ? BlockData.colors[preflatMap.blocks[`${block.type}:${block.metadata}`]?.replaceAll(/\[.*?]/g, '')] ?? 'rgb(0, 0, 255)' : this.blockData[block.name] ?? 'rgb(0, 255, 0)'
|
||||
colors[index] = color
|
||||
colors[index] = this.setColor(block)
|
||||
}
|
||||
}
|
||||
const chunk: ChunkInfo = { heightmap, colors }
|
||||
|
|
@ -302,17 +294,12 @@ export class DrawerAdapterImpl extends TypedEventEmitter<MapUpdates> implements
|
|||
return chunk
|
||||
}
|
||||
|
||||
getSingleplayerRootPath (): string | undefined {
|
||||
return localServer!.options.worldFolder
|
||||
}
|
||||
|
||||
async getChunkHeightMapFromRegion (chunkX: number, chunkZ: number, cb?: (hm: number[]) => void) {
|
||||
const regionX = Math.floor(chunkX / 32)
|
||||
const regionZ = Math.floor(chunkZ / 32)
|
||||
const regionKey = `${regionX},${regionZ}`
|
||||
if (!this.regions.has(regionKey)) {
|
||||
const worldFolder = this.getSingleplayerRootPath()
|
||||
if (!worldFolder) return
|
||||
const { worldFolder } = localServer!.options
|
||||
const path = `${worldFolder}/region/r.${regionX}.${regionZ}.mca`
|
||||
const region = new RegionFile(path)
|
||||
await region.initialize()
|
||||
|
|
@ -350,8 +337,7 @@ export class DrawerAdapterImpl extends TypedEventEmitter<MapUpdates> implements
|
|||
continue
|
||||
}
|
||||
heightmap[index] = hBlock.y
|
||||
const color = this.isOldVersion ? BlockData.colors[preflatMap.blocks[`${block.type}:${block.metadata}`]?.replaceAll(/\[.*?]/g, '')] ?? 'rgb(0, 0, 255)' : this.blockData[block.name] ?? 'rgb(0, 255, 0)'
|
||||
colors[index] = color
|
||||
colors[index] = this.setColor(block)
|
||||
}
|
||||
}
|
||||
const chunk = { heightmap, colors }
|
||||
|
|
@ -456,6 +442,32 @@ export class DrawerAdapterImpl extends TypedEventEmitter<MapUpdates> implements
|
|||
}
|
||||
}
|
||||
|
||||
setColor (block: Block) {
|
||||
let color: string
|
||||
if (this.isOldVersion) {
|
||||
color = BlockData.colors[preflatMap.blocks[`${block.type}:${block.metadata}`]?.replaceAll(/\[.*?]/g, '')]
|
||||
?? 'rgb(0, 0, 255)'
|
||||
} else {
|
||||
color = this.blockData.get(block.name) ?? 'rgb(0, 255, 0)'
|
||||
}
|
||||
if (color === 'rgb(0, 255, 0)' || color === 'rgb(0, 0, 255)') {
|
||||
// this should never happen
|
||||
// console.warn('[MinimapProvider] did not find block name,', block.name)
|
||||
// hack to find close color. Problem with colors should be fixed differently in the future
|
||||
const blockNamePieces = block.name.split('_')
|
||||
const keys = [...this.blockData.keys()]
|
||||
for (const piece of blockNamePieces) {
|
||||
const match = keys.find(x => x.includes(piece))
|
||||
if (match) {
|
||||
color = this.blockData.get(match) ?? 'rgb(255, 0, 0)'
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return color
|
||||
}
|
||||
|
||||
quickTp (x: number, z: number) {
|
||||
const y = this.getHighestBlockY(x, z)
|
||||
bot.chat(`/tp ${x} ${y + 20} ${z}`)
|
||||
|
|
@ -467,25 +479,6 @@ export class DrawerAdapterImpl extends TypedEventEmitter<MapUpdates> implements
|
|||
}
|
||||
|
||||
async drawChunkOnCanvas (key: string, canvas: HTMLCanvasElement) {
|
||||
// console.log('chunk', key, 'on canvas')
|
||||
if (!this.loadChunkFullmap) {
|
||||
// wait for it to be available
|
||||
await new Promise(resolve => {
|
||||
const interval = setInterval(() => {
|
||||
if (this.loadChunkFullmap) {
|
||||
clearInterval(interval)
|
||||
resolve(undefined)
|
||||
}
|
||||
}, 100)
|
||||
setTimeout(() => {
|
||||
clearInterval(interval)
|
||||
resolve(undefined)
|
||||
}, 10_000)
|
||||
})
|
||||
if (!this.loadChunkFullmap) {
|
||||
throw new Error('loadChunkFullmap not available')
|
||||
}
|
||||
}
|
||||
const chunk = await this.loadChunkFullmap(key)
|
||||
const [worldX, worldZ] = key.split(',').map(x => Number(x) * 16)
|
||||
const center = new Vec3(worldX + 8, 0, worldZ + 8)
|
||||
|
|
@ -493,6 +486,10 @@ export class DrawerAdapterImpl extends TypedEventEmitter<MapUpdates> implements
|
|||
this.mapDrawer.canvas = canvas
|
||||
this.mapDrawer.full = true
|
||||
this.mapDrawer.drawChunk(key, chunk)
|
||||
this.mapDrawer.drawWarps(center)
|
||||
this.mapDrawer.lastBotPos = this.playerPosition
|
||||
this.mapDrawer.yaw = this.yaw
|
||||
this.mapDrawer.drawPlayerPos(worldX, worldZ)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -501,7 +498,7 @@ const Inner = (
|
|||
{
|
||||
adapter: DrawerAdapterImpl
|
||||
displayMode?: DisplayMode,
|
||||
toggleFullMap?: ({ command }: { command?: string }) => void
|
||||
toggleFullMap?: () => void
|
||||
}
|
||||
) => {
|
||||
|
||||
|
|
@ -515,7 +512,7 @@ const Inner = (
|
|||
}
|
||||
|
||||
const updateMap = () => {
|
||||
if (!adapter) return
|
||||
if (!adapter || miscUiState.displayFullmap) return
|
||||
adapter.playerPosition = bot.entity.position
|
||||
adapter.yaw = bot.entity.yaw
|
||||
adapter.emit('updateMap')
|
||||
|
|
@ -544,51 +541,13 @@ const Inner = (
|
|||
</div>
|
||||
}
|
||||
|
||||
export default ({ displayMode }: { displayMode?: DisplayMode }) => {
|
||||
const [adapter] = useState(() => new DrawerAdapterImpl(bot.entity.position))
|
||||
export default ({ adapter, displayMode }: { adapter: DrawerAdapterImpl, displayMode?: DisplayMode }) => {
|
||||
|
||||
const { showMinimap } = useSnapshot(options)
|
||||
const fullMapOpened = useIsModalActive('full-map')
|
||||
|
||||
|
||||
const readChunksHeightMaps = async () => {
|
||||
const { worldFolder } = localServer!.options
|
||||
const path = `${worldFolder}/region/r.0.0.mca`
|
||||
const region = new RegionFile(path)
|
||||
await region.initialize()
|
||||
const chunks: Record<string, any> = {}
|
||||
console.log('Reading chunks...')
|
||||
console.log(chunks)
|
||||
let versionDetected = false
|
||||
for (const [i, _] of Array.from({ length: 32 }).entries()) {
|
||||
for (const [k, _] of Array.from({ length: 32 }).entries()) {
|
||||
// todo, may use faster reading, but features is not commonly used
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const nbt = await region.read(i, k)
|
||||
chunks[`${i},${k}`] = nbt
|
||||
if (nbt && !versionDetected) {
|
||||
const simplified = simplify(nbt)
|
||||
const version = versions.pc.find(x => x['dataVersion'] === simplified.DataVersion)?.minecraftVersion
|
||||
console.log('Detected version', version ?? 'unknown')
|
||||
versionDetected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
Object.defineProperty(chunks, 'simplified', {
|
||||
get () {
|
||||
const mapped = {}
|
||||
for (const [i, _] of Array.from({ length: 32 }).entries()) {
|
||||
for (const [k, _] of Array.from({ length: 32 }).entries()) {
|
||||
const key = `${i},${k}`
|
||||
const chunk = chunks[key]
|
||||
if (!chunk) continue
|
||||
mapped[key] = simplify(chunk)
|
||||
}
|
||||
}
|
||||
return mapped
|
||||
},
|
||||
})
|
||||
console.log('Done!', chunks)
|
||||
const toggleFullMap = () => {
|
||||
void contro.emit('trigger', { command: 'ui.toggleMap', schema: null as any })
|
||||
}
|
||||
|
||||
if (
|
||||
|
|
@ -599,13 +558,5 @@ export default ({ displayMode }: { displayMode?: DisplayMode }) => {
|
|||
return null
|
||||
}
|
||||
|
||||
const toggleFullMap = () => {
|
||||
if (activeModalStack.at(-1)?.reactType === 'full-map') {
|
||||
hideModal({ reactType: 'full-map' })
|
||||
} else {
|
||||
showModal({ reactType: 'full-map' })
|
||||
}
|
||||
}
|
||||
|
||||
return <Inner adapter={adapter} displayMode={displayMode} toggleFullMap={toggleFullMap} />
|
||||
}
|
||||
|
|
|
|||
50
src/react/NetworkStatus.module.css
Normal file
50
src/react/NetworkStatus.module.css
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
.container {
|
||||
scale: 0.8;
|
||||
transform-origin: left;
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto auto auto;
|
||||
gap: 2px 4px;
|
||||
font-size: 8px;
|
||||
color: #fff;
|
||||
opacity: 0.8;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.container.websocket {
|
||||
grid-template-columns: auto auto auto;
|
||||
}
|
||||
|
||||
.iconRow {
|
||||
display: block;
|
||||
}
|
||||
.arrowRow {
|
||||
scale: 1.5 1;
|
||||
}
|
||||
|
||||
.dataRow {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 70px;
|
||||
font-size: 5px;
|
||||
}
|
||||
|
||||
.ping {
|
||||
font-size: 6px;
|
||||
}
|
||||
|
||||
/* .dataRow > span:nth-child(3) {
|
||||
max-width: 120px;
|
||||
} */
|
||||
|
||||
.totalRow {
|
||||
grid-column: span 3;
|
||||
font-size: 7px;
|
||||
}
|
||||
|
||||
.stale {
|
||||
color: #ff4444;
|
||||
}
|
||||
14
src/react/NetworkStatus.module.css.d.ts
vendored
Normal file
14
src/react/NetworkStatus.module.css.d.ts
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
arrowRow: string;
|
||||
container: string;
|
||||
dataRow: string;
|
||||
iconRow: string;
|
||||
ping: string;
|
||||
stale: string;
|
||||
totalRow: string;
|
||||
websocket: string;
|
||||
}
|
||||
declare const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
102
src/react/NetworkStatus.tsx
Normal file
102
src/react/NetworkStatus.tsx
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import { useEffect, useMemo, useState, useRef } from 'react'
|
||||
import { parseServerAddress } from '../parseServerAddress'
|
||||
import { lastConnectOptions } from './AppStatusProvider'
|
||||
import PixelartIcon, { pixelartIcons } from './PixelartIcon'
|
||||
import styles from './NetworkStatus.module.css'
|
||||
|
||||
export default () => {
|
||||
const [proxyPing, setProxyPing] = useState<number | null>(null)
|
||||
const [serverPing, setServerPing] = useState<number | null>(null)
|
||||
const [isProxyStale, setIsProxyStale] = useState(false)
|
||||
const [isServerStale, setIsServerStale] = useState(false)
|
||||
|
||||
const proxyTimeoutRef = useRef<NodeJS.Timeout>()
|
||||
const serverTimeoutRef = useRef<NodeJS.Timeout>()
|
||||
|
||||
const isWebSocket = useMemo(() => parseServerAddress(lastConnectOptions.value?.server).isWebSocket, [lastConnectOptions.value?.server])
|
||||
const serverIp = useMemo(() => lastConnectOptions.value?.server, [])
|
||||
|
||||
const setProxyPingWithTimeout = (ping: number | null) => {
|
||||
setProxyPing(ping)
|
||||
setIsProxyStale(false)
|
||||
if (proxyTimeoutRef.current) clearTimeout(proxyTimeoutRef.current)
|
||||
proxyTimeoutRef.current = setTimeout(() => setIsProxyStale(true), 1000)
|
||||
}
|
||||
|
||||
const setServerPingWithTimeout = (ping: number | null) => {
|
||||
setServerPing(ping)
|
||||
setIsServerStale(false)
|
||||
if (serverTimeoutRef.current) clearTimeout(serverTimeoutRef.current)
|
||||
serverTimeoutRef.current = setTimeout(() => setIsServerStale(true), 1000)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!serverIp) return
|
||||
|
||||
const updatePing = async () => {
|
||||
const updateServerPing = async () => {
|
||||
const ping = await bot.pingServer()
|
||||
if (ping) {
|
||||
setServerPingWithTimeout(ping)
|
||||
}
|
||||
}
|
||||
|
||||
const updateProxyPing = async () => {
|
||||
if (!isWebSocket) {
|
||||
const ping = await bot.pingProxy()
|
||||
setProxyPingWithTimeout(ping)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all([updateServerPing(), updateProxyPing()])
|
||||
} catch (err) {
|
||||
console.error('Failed to ping:', err)
|
||||
}
|
||||
}
|
||||
|
||||
void updatePing()
|
||||
const interval = setInterval(updatePing, 1000)
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
if (proxyTimeoutRef.current) clearTimeout(proxyTimeoutRef.current)
|
||||
if (serverTimeoutRef.current) clearTimeout(serverTimeoutRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (!serverIp) return null
|
||||
|
||||
const { username } = bot.player
|
||||
const { proxy: proxyUrl } = lastConnectOptions.value!
|
||||
const pingTotal = serverPing
|
||||
|
||||
const ICON_SIZE = 18
|
||||
|
||||
return (
|
||||
<div className={`${styles.container} ${isWebSocket ? styles.websocket : ''}`}>
|
||||
<PixelartIcon className={styles.iconRow} iconName={pixelartIcons.user} width={ICON_SIZE} />
|
||||
{!isWebSocket && (
|
||||
<>
|
||||
<PixelartIcon className={`${styles.iconRow} ${styles.arrowRow}`} iconName={pixelartIcons['arrow-right']} width={16} />
|
||||
<PixelartIcon className={styles.iconRow} iconName={pixelartIcons.server} width={ICON_SIZE} />
|
||||
</>
|
||||
)}
|
||||
<PixelartIcon className={`${styles.iconRow} ${styles.arrowRow}`} iconName={pixelartIcons['arrow-right']} width={16} />
|
||||
<PixelartIcon className={styles.iconRow} iconName={pixelartIcons['list-box']} width={ICON_SIZE} />
|
||||
|
||||
<span className={styles.dataRow}>{username}</span>
|
||||
{!isWebSocket && (
|
||||
<>
|
||||
<span className={`${styles.dataRow} ${styles.ping} ${isProxyStale ? styles.stale : ''}`}>{proxyPing}ms</span>
|
||||
<span className={styles.dataRow}>{proxyUrl}</span>
|
||||
</>
|
||||
)}
|
||||
<span className={`${styles.dataRow} ${styles.ping} ${isServerStale ? styles.stale : ''}`}>
|
||||
{isWebSocket ? (pingTotal || '?') : (pingTotal ? pingTotal - (proxyPing ?? 0) : '...')}ms
|
||||
</span>
|
||||
<span className={styles.dataRow}>{serverIp}</span>
|
||||
|
||||
<span className={styles.totalRow}>Ping: {pingTotal || '?'}ms</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -8,17 +8,24 @@ const componentActive = proxy({
|
|||
enabled: false
|
||||
})
|
||||
|
||||
subscribe(activeModalStack, () => {
|
||||
const checkModalAvailability = () => {
|
||||
const last = activeModalStack.at(-1)
|
||||
let withWildCardModal = false
|
||||
for (const modal of watchedModalsFromHooks) {
|
||||
for (const modal of watchedModalsFromHooks.value) {
|
||||
if (modal.endsWith('*') && last?.reactType.startsWith(modal.slice(0, -1))) {
|
||||
withWildCardModal = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
componentActive.enabled = !!last && !hardcodedKnownModals.some(x => last.reactType.startsWith(x)) && !watchedModalsFromHooks.has(last.reactType) && !withWildCardModal
|
||||
componentActive.enabled = !!last && !hardcodedKnownModals.some(x => last.reactType.startsWith(x)) && !watchedModalsFromHooks.value.has(last.reactType) && !withWildCardModal
|
||||
}
|
||||
|
||||
subscribe(activeModalStack, () => {
|
||||
checkModalAvailability()
|
||||
})
|
||||
subscribe(watchedModalsFromHooks, () => {
|
||||
checkModalAvailability()
|
||||
})
|
||||
|
||||
export default () => {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ import {
|
|||
showModal,
|
||||
hideModal,
|
||||
miscUiState,
|
||||
openOptionsMenu
|
||||
openOptionsMenu,
|
||||
gameAdditionalState
|
||||
} from '../globalState'
|
||||
import { fsState } from '../loadSave'
|
||||
import { disconnect } from '../flyingSquidUtils'
|
||||
|
|
@ -28,7 +29,8 @@ import Screen from './Screen'
|
|||
import styles from './PauseScreen.module.css'
|
||||
import { DiscordButton } from './DiscordButton'
|
||||
import { showNotification } from './NotificationProvider'
|
||||
import { appStatusState } from './AppStatusProvider'
|
||||
import { appStatusState, reconnectReload } from './AppStatusProvider'
|
||||
import NetworkStatus from './NetworkStatus'
|
||||
|
||||
const waitForPotentialRender = async () => {
|
||||
return new Promise<void>(resolve => {
|
||||
|
|
@ -153,6 +155,7 @@ export default () => {
|
|||
const fsStateSnap = useSnapshot(fsState)
|
||||
const activeModalStackSnap = useSnapshot(activeModalStack)
|
||||
const { singleplayer, wanOpened, wanOpening } = useSnapshot(miscUiState)
|
||||
const { noConnection } = useSnapshot(gameAdditionalState)
|
||||
|
||||
const handlePointerLockChange = () => {
|
||||
if (!pointerLock.hasPointerLock && activeModalStack.length === 0) {
|
||||
|
|
@ -224,6 +227,9 @@ export default () => {
|
|||
style={{ position: 'fixed', top: '5px', left: 'calc(env(safe-area-inset-left) + 5px)' }}
|
||||
onClick={async () => openWorldActions()}
|
||||
/>
|
||||
<div style={{ position: 'fixed', top: '5px', left: 'calc(env(safe-area-inset-left) + 35px)' }}>
|
||||
<NetworkStatus />
|
||||
</div>
|
||||
<div className={styles.pause_container}>
|
||||
<Button className="button" style={{ width: '204px' }} onClick={onReturnPress}>Back to Game</Button>
|
||||
<div className={styles.row}>
|
||||
|
|
@ -259,6 +265,11 @@ export default () => {
|
|||
{localServer && !fsState.syncFs && !fsState.isReadonly ? 'Save & Quit' : 'Disconnect & Reset'}
|
||||
</Button>
|
||||
</>}
|
||||
{noConnection && (
|
||||
<Button className="button" style={{ width: '204px' }} onClick={reconnectReload}>
|
||||
Reconnect
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Screen>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export default ({
|
|||
className = undefined as undefined | string,
|
||||
onClick = () => { }
|
||||
}) => {
|
||||
if (width !== undefined) styles = { width, height: width, ...styles }
|
||||
if (width !== undefined) styles = { width, height: width, fontSize: width, ...styles }
|
||||
iconName = iconName.replace('pixelarticons:', '')
|
||||
|
||||
return <div
|
||||
|
|
|
|||
|
|
@ -53,6 +53,13 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
const [quickConnectIp, setQuickConnectIp] = useState('')
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
|
||||
// Save username to localStorage when component mounts if it doesn't exist
|
||||
useEffect(() => {
|
||||
if (!localStorage['username']) {
|
||||
localStorage.setItem('username', defaultUsername)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const setAuthenticatedAccounts = (newState: typeof authenticatedAccounts) => {
|
||||
_setAuthenticatedAccounts(newState)
|
||||
localStorage.setItem('authenticatedAccounts', JSON.stringify(newState))
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ import SignEditor, { ResultType } from './SignEditor'
|
|||
|
||||
|
||||
const isWysiwyg = async () => {
|
||||
const items = await bot.tabComplete('/', true, true)
|
||||
const commands = new Set<string>(['data'])
|
||||
const items = await bot.tabComplete('/data ', true, true)
|
||||
const commands = new Set<string>(['merge'])
|
||||
for (const item of items) {
|
||||
if (commands.has(item.match as unknown as string)) {
|
||||
if (commands.has((item.match ?? item) as unknown as string)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
30
src/react/UIProvider.tsx
Normal file
30
src/react/UIProvider.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
import { useMedia } from 'react-use'
|
||||
|
||||
export const ScaleContext = createContext<number>(1)
|
||||
|
||||
export const useScale = () => useContext(ScaleContext)
|
||||
|
||||
export const UIProvider = ({ children, scale = 1 }) => {
|
||||
return (
|
||||
<ScaleContext.Provider value={scale}>
|
||||
{children}
|
||||
</ScaleContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export const usePassesScaledDimensions = (minWidth: number | null = null, minHeight: number | null = null) => {
|
||||
const scale = useScale()
|
||||
const conditions: string[] = []
|
||||
|
||||
if (minWidth !== null) {
|
||||
conditions.push(`(min-width: ${minWidth * scale}px)`)
|
||||
}
|
||||
if (minHeight !== null) {
|
||||
conditions.push(`(min-height: ${minHeight * scale}px)`)
|
||||
}
|
||||
|
||||
const media = conditions.join(' and ') || 'all'
|
||||
return useMedia(media)
|
||||
}
|
||||
62
src/react/hooks/useScrollBehavior.ts
Normal file
62
src/react/hooks/useScrollBehavior.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { RefObject, useEffect, useLayoutEffect, useRef } from 'react'
|
||||
import { pixelartIcons } from '../PixelartIcon'
|
||||
|
||||
export const useScrollBehavior = (
|
||||
elementRef: RefObject<HTMLElement>,
|
||||
{
|
||||
messages,
|
||||
opened
|
||||
}: {
|
||||
messages: readonly any[],
|
||||
opened?: boolean
|
||||
}
|
||||
) => {
|
||||
const openedWasAtBottom = useRef(true) // before new messages
|
||||
|
||||
const isAtBottom = () => {
|
||||
if (!elementRef.current) return true
|
||||
const { scrollTop, scrollHeight, clientHeight } = elementRef.current
|
||||
const distanceFromBottom = Math.abs(scrollHeight - clientHeight - scrollTop)
|
||||
return distanceFromBottom < 1
|
||||
}
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (elementRef.current) {
|
||||
elementRef.current.scrollTop = elementRef.current.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
// Handle scroll position tracking
|
||||
useEffect(() => {
|
||||
const element = elementRef.current
|
||||
if (!element) return
|
||||
|
||||
const handleScroll = () => {
|
||||
openedWasAtBottom.current = isAtBottom()
|
||||
}
|
||||
|
||||
element.addEventListener('scroll', handleScroll)
|
||||
return () => element.removeEventListener('scroll', handleScroll)
|
||||
}, [])
|
||||
|
||||
// Handle opened state changes
|
||||
useLayoutEffect(() => {
|
||||
if (opened) {
|
||||
openedWasAtBottom.current = true
|
||||
} else {
|
||||
scrollToBottom()
|
||||
}
|
||||
}, [opened])
|
||||
|
||||
// Handle messages changes
|
||||
useLayoutEffect(() => {
|
||||
if ((!opened || (opened && openedWasAtBottom.current)) && elementRef.current) {
|
||||
scrollToBottom()
|
||||
}
|
||||
}, [messages])
|
||||
|
||||
return {
|
||||
scrollToBottom,
|
||||
isAtBottom
|
||||
}
|
||||
}
|
||||
|
|
@ -7,8 +7,7 @@ export const motionState = proxy({
|
|||
})
|
||||
|
||||
const MOTION_DAMPING = 0.92
|
||||
// const MAX_MOTION_OFFSET = 30
|
||||
const MAX_MOTION_OFFSET = 350
|
||||
const MAX_MOTION_OFFSET = 100
|
||||
const motionVelocity = { x: 0, y: 0 }
|
||||
let lastUpdate = performance.now()
|
||||
let lastYaw = 0
|
||||
|
|
@ -39,13 +38,13 @@ export function updateMotion () {
|
|||
|
||||
// Calculate motion contribution
|
||||
const velocityContribution = {
|
||||
x: -velocityVector.x * 200,
|
||||
y: -velocityVector.z * 200
|
||||
x: -velocityVector.x * 150,
|
||||
y: -velocityVector.z * 150
|
||||
}
|
||||
|
||||
// Combine camera and velocity effects
|
||||
motionVelocity.x += (yawDiff * 400 + velocityContribution.x) * deltaTime
|
||||
motionVelocity.y += (pitchDiff * 400 + velocityContribution.y) * deltaTime
|
||||
motionVelocity.x += (yawDiff * 300 + velocityContribution.x) * deltaTime
|
||||
motionVelocity.y += (pitchDiff * 300 + velocityContribution.y) * deltaTime
|
||||
|
||||
// Apply damping
|
||||
motionVelocity.x *= MOTION_DAMPING
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { useSnapshot } from 'valtio'
|
||||
import { proxy, useSnapshot } from 'valtio'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { useMedia } from 'react-use'
|
||||
import { activeModalStack, miscUiState } from '../globalState'
|
||||
|
||||
export const watchedModalsFromHooks = new Set<string>()
|
||||
export const watchedModalsFromHooks = proxy({
|
||||
value: new Set<string>()
|
||||
})
|
||||
// todo should not be there
|
||||
export const hardcodedKnownModals = [
|
||||
'player_win:',
|
||||
|
|
@ -15,12 +17,12 @@ export const useUsingTouch = () => {
|
|||
}
|
||||
export const useIsModalActive = (modal: string, useIncludes = false) => {
|
||||
useMemo(() => {
|
||||
watchedModalsFromHooks.add(modal)
|
||||
watchedModalsFromHooks.value.add(modal)
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
// watchedModalsFromHooks.add(modal)
|
||||
return () => {
|
||||
watchedModalsFromHooks.delete(modal)
|
||||
watchedModalsFromHooks.value.delete(modal)
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import ScoreboardProvider from './react/ScoreboardProvider'
|
|||
import SignEditorProvider from './react/SignEditorProvider'
|
||||
import IndicatorEffectsProvider from './react/IndicatorEffectsProvider'
|
||||
import PlayerListOverlayProvider from './react/PlayerListOverlayProvider'
|
||||
import MinimapProvider from './react/MinimapProvider'
|
||||
import MinimapProvider, { DrawerAdapterImpl } from './react/MinimapProvider'
|
||||
import HudBarsProvider from './react/HudBarsProvider'
|
||||
import XPBarProvider from './react/XPBarProvider'
|
||||
import DebugOverlay from './react/DebugOverlay'
|
||||
|
|
@ -49,6 +49,8 @@ import DebugEdges from './react/DebugEdges'
|
|||
import GameInteractionOverlay from './react/GameInteractionOverlay'
|
||||
import MineflayerPluginHud from './react/MineflayerPluginHud'
|
||||
import MineflayerPluginConsole from './react/MineflayerPluginConsole'
|
||||
import { UIProvider } from './react/UIProvider'
|
||||
import { useAppScale } from './scaleInterface'
|
||||
|
||||
const RobustPortal = ({ children, to }) => {
|
||||
return createPortal(<PerComponentErrorBoundary>{children}</PerComponentErrorBoundary>, to)
|
||||
|
|
@ -104,17 +106,22 @@ const InGameComponent = ({ children }) => {
|
|||
return children
|
||||
}
|
||||
|
||||
// for Fullmap and Minimap in InGameUi
|
||||
let adapter: DrawerAdapterImpl
|
||||
|
||||
const InGameUi = () => {
|
||||
const { gameLoaded, showUI: showUIRaw } = useSnapshot(miscUiState)
|
||||
const { disabledUiParts, displayBossBars, showMinimap } = useSnapshot(options)
|
||||
const modalsSnapshot = useSnapshot(activeModalStack)
|
||||
const hasModals = modalsSnapshot.length > 0
|
||||
const showUI = showUIRaw || hasModals
|
||||
const displayFullmap = modalsSnapshot.some(modal => modal.reactType === 'full-map')
|
||||
const displayFullmap = modalsSnapshot.some(modal => modal.reactType === 'full-map') || true
|
||||
// bot can't be used here
|
||||
|
||||
if (!gameLoaded || !bot || disabledUiParts.includes('*')) return
|
||||
|
||||
if (!adapter) adapter = new DrawerAdapterImpl(bot.entity.position)
|
||||
|
||||
return <>
|
||||
<RobustPortal to={document.querySelector('#ui-root')}>
|
||||
{/* apply scaling */}
|
||||
|
|
@ -126,7 +133,7 @@ const InGameUi = () => {
|
|||
{!disabledUiParts.includes('players-list') && <PlayerListOverlayProvider />}
|
||||
{!disabledUiParts.includes('chat') && <ChatProvider />}
|
||||
<SoundMuffler />
|
||||
{showMinimap !== 'never' && <MinimapProvider displayMode='minimapOnly' />}
|
||||
{showMinimap !== 'never' && <MinimapProvider adapter={adapter} displayMode='minimapOnly' />}
|
||||
{!disabledUiParts.includes('title') && <TitleProvider />}
|
||||
{!disabledUiParts.includes('scoreboard') && <ScoreboardProvider />}
|
||||
{!disabledUiParts.includes('effects-indicators') && <IndicatorEffectsProvider />}
|
||||
|
|
@ -150,7 +157,7 @@ const InGameUi = () => {
|
|||
<DisplayQr />
|
||||
</PerComponentErrorBoundary>
|
||||
<RobustPortal to={document.body}>
|
||||
{displayFullmap && <MinimapProvider displayMode='fullmapOnly' />}
|
||||
{displayFullmap && <MinimapProvider adapter={adapter} displayMode='fullmapOnly' />}
|
||||
{/* because of z-index */}
|
||||
{showUI && <TouchControls />}
|
||||
<GlobalSearchInput />
|
||||
|
|
@ -170,45 +177,47 @@ const WidgetDisplay = ({ name, Component }) => {
|
|||
}
|
||||
|
||||
const App = () => {
|
||||
return <div>
|
||||
<ButtonAppProvider>
|
||||
<RobustPortal to={document.body}>
|
||||
<div className='overlay-bottom-scaled'>
|
||||
<InGameComponent>
|
||||
<HeldMapUi />
|
||||
</InGameComponent>
|
||||
</div>
|
||||
<div />
|
||||
</RobustPortal>
|
||||
<EnterFullscreenButton />
|
||||
<InGameUi />
|
||||
<RobustPortal to={document.querySelector('#ui-root')}>
|
||||
<AllWidgets />
|
||||
<SingleplayerProvider />
|
||||
<CreateWorldProvider />
|
||||
<AppStatusProvider />
|
||||
<KeybindingsScreenProvider />
|
||||
<SelectOption />
|
||||
<ServersListProvider />
|
||||
<OptionsRenderApp />
|
||||
<MainMenuRenderApp />
|
||||
<NotificationProvider />
|
||||
<TouchAreasControlsProvider />
|
||||
<SignInMessageProvider />
|
||||
<NoModalFoundProvider />
|
||||
{/* <GameHud>
|
||||
</GameHud> */}
|
||||
</RobustPortal>
|
||||
<RobustPortal to={document.body}>
|
||||
{/* todo correct mounting! */}
|
||||
<div className='overlay-top-scaled'>
|
||||
<GamepadUiCursor />
|
||||
</div>
|
||||
<div />
|
||||
<DebugEdges />
|
||||
</RobustPortal>
|
||||
</ButtonAppProvider>
|
||||
</div>
|
||||
const scale = useAppScale()
|
||||
return (
|
||||
<UIProvider scale={scale}>
|
||||
<div>
|
||||
<ButtonAppProvider>
|
||||
<RobustPortal to={document.body}>
|
||||
<div className='overlay-bottom-scaled'>
|
||||
<InGameComponent>
|
||||
<HeldMapUi />
|
||||
</InGameComponent>
|
||||
</div>
|
||||
<div />
|
||||
</RobustPortal>
|
||||
<EnterFullscreenButton />
|
||||
<InGameUi />
|
||||
<RobustPortal to={document.querySelector('#ui-root')}>
|
||||
<AllWidgets />
|
||||
<SingleplayerProvider />
|
||||
<CreateWorldProvider />
|
||||
<AppStatusProvider />
|
||||
<KeybindingsScreenProvider />
|
||||
<SelectOption />
|
||||
<ServersListProvider />
|
||||
<OptionsRenderApp />
|
||||
<MainMenuRenderApp />
|
||||
<NotificationProvider />
|
||||
<TouchAreasControlsProvider />
|
||||
<SignInMessageProvider />
|
||||
<NoModalFoundProvider />
|
||||
</RobustPortal>
|
||||
<RobustPortal to={document.body}>
|
||||
<div className='overlay-top-scaled'>
|
||||
<GamepadUiCursor />
|
||||
</div>
|
||||
<div />
|
||||
<DebugEdges />
|
||||
</RobustPortal>
|
||||
</ButtonAppProvider>
|
||||
</div>
|
||||
</UIProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const PerComponentErrorBoundary = ({ children }) => {
|
||||
|
|
|
|||
|
|
@ -1,67 +1,96 @@
|
|||
import { subscribeKey } from 'valtio/utils'
|
||||
import { gameAdditionalState } from './globalState'
|
||||
import { options } from './optionsStorage'
|
||||
import { playerState } from './mineflayer/playerState'
|
||||
|
||||
let currentFov = 0
|
||||
let targetFov = 0
|
||||
let lastUpdateTime = 0
|
||||
const FOV_TRANSITION_DURATION = 200 // milliseconds
|
||||
|
||||
// TODO: These should be configured based on your game's settings
|
||||
const BASE_MOVEMENT_SPEED = 0.1 // Default walking speed in Minecraft
|
||||
const FOV_EFFECT_SCALE = 1 // Equivalent to Minecraft's FOV Effects slider
|
||||
|
||||
const updateFovAnimation = () => {
|
||||
if (currentFov === targetFov) return
|
||||
if (!bot) return
|
||||
|
||||
const now = performance.now()
|
||||
const elapsed = now - lastUpdateTime
|
||||
const progress = Math.min(elapsed / FOV_TRANSITION_DURATION, 1)
|
||||
// Calculate base FOV modifier
|
||||
let fovModifier = 1
|
||||
|
||||
// Smooth easing function
|
||||
const easeOutCubic = (t: number) => 1 - (1 - t) ** 3
|
||||
|
||||
currentFov += (targetFov - currentFov) * easeOutCubic(progress)
|
||||
|
||||
if (Math.abs(currentFov - targetFov) < 0.01) {
|
||||
currentFov = targetFov
|
||||
// Flying modifier
|
||||
if (gameAdditionalState.isFlying) {
|
||||
fovModifier *= 1.05
|
||||
}
|
||||
|
||||
viewer.camera.fov = currentFov
|
||||
viewer.camera.updateProjectionMatrix()
|
||||
// Movement speed modifier
|
||||
// TODO: Get actual movement speed attribute value
|
||||
const movementSpeedAttr = (bot.entity?.attributes?.['generic.movement_speed'] || bot.entity?.attributes?.['minecraft:movement_speed'] || bot.entity?.attributes?.['movement_speed'] || bot.entity?.attributes?.['minecraft:movementSpeed'])?.value || BASE_MOVEMENT_SPEED
|
||||
let currentSpeed = BASE_MOVEMENT_SPEED
|
||||
// todo
|
||||
if (bot.controlState?.sprint && !bot.controlState?.sneak) {
|
||||
currentSpeed *= 1.3
|
||||
}
|
||||
fovModifier *= (currentSpeed / movementSpeedAttr + 1) / 2
|
||||
|
||||
// Validate fov modifier
|
||||
if (Math.abs(BASE_MOVEMENT_SPEED) < Number.EPSILON || isNaN(fovModifier) || !isFinite(fovModifier)) {
|
||||
fovModifier = 1
|
||||
}
|
||||
|
||||
// Item usage modifier
|
||||
if (playerState.getHeldItem()) {
|
||||
const heldItem = playerState.getHeldItem()
|
||||
if (heldItem?.name === 'bow' && playerState.getItemUsageTicks() > 0) {
|
||||
const ticksUsingItem = playerState.getItemUsageTicks()
|
||||
let usageProgress = ticksUsingItem / 20
|
||||
if (usageProgress > 1) {
|
||||
usageProgress = 1
|
||||
} else {
|
||||
usageProgress *= usageProgress
|
||||
}
|
||||
fovModifier *= 1 - usageProgress * 0.15
|
||||
}
|
||||
// TODO: Add spyglass/scope check here if needed
|
||||
}
|
||||
|
||||
// Apply FOV effect scale
|
||||
fovModifier = 1 + (fovModifier - 1) * FOV_EFFECT_SCALE
|
||||
|
||||
// Calculate target FOV
|
||||
const baseFov = gameAdditionalState.isZooming ? 30 : options.fov
|
||||
targetFov = baseFov * fovModifier
|
||||
|
||||
// Smooth transition
|
||||
const now = performance.now()
|
||||
if (currentFov !== targetFov) {
|
||||
const elapsed = now - lastUpdateTime
|
||||
const progress = Math.min(elapsed / FOV_TRANSITION_DURATION, 1)
|
||||
const easeOutCubic = (t: number) => 1 - (1 - t) ** 3
|
||||
|
||||
currentFov += (targetFov - currentFov) * easeOutCubic(progress)
|
||||
|
||||
if (Math.abs(currentFov - targetFov) < 0.01) {
|
||||
currentFov = targetFov
|
||||
}
|
||||
|
||||
viewer.camera.fov = currentFov
|
||||
viewer.camera.updateProjectionMatrix()
|
||||
}
|
||||
lastUpdateTime = now
|
||||
}
|
||||
|
||||
export const watchFov = () => {
|
||||
const updateFov = () => {
|
||||
if (!bot) return
|
||||
let fov = gameAdditionalState.isZooming ? 30 : options.fov
|
||||
|
||||
if (bot.controlState.sprint && !bot.controlState.sneak) {
|
||||
fov += 5
|
||||
}
|
||||
if (gameAdditionalState.isFlying) {
|
||||
fov += 5
|
||||
}
|
||||
|
||||
if (targetFov !== fov) {
|
||||
targetFov = fov
|
||||
lastUpdateTime = performance.now()
|
||||
}
|
||||
}
|
||||
|
||||
customEvents.on('gameLoaded', () => {
|
||||
updateFov()
|
||||
})
|
||||
|
||||
updateFov()
|
||||
|
||||
// Add FOV animation to render loop
|
||||
// Initial FOV setup
|
||||
if (!beforeRenderFrame.includes(updateFovAnimation)) {
|
||||
beforeRenderFrame.push(updateFovAnimation)
|
||||
}
|
||||
|
||||
subscribeKey(options, 'fov', updateFov)
|
||||
subscribeKey(gameAdditionalState, 'isFlying', updateFov)
|
||||
subscribeKey(gameAdditionalState, 'isSprinting', updateFov)
|
||||
subscribeKey(gameAdditionalState, 'isZooming', updateFov)
|
||||
customEvents.on('gameLoaded', () => {
|
||||
updateFovAnimation()
|
||||
})
|
||||
|
||||
subscribeKey(gameAdditionalState, 'isSneaking', () => {
|
||||
viewer.isSneaking = gameAdditionalState.isSneaking
|
||||
viewer.setFirstPersonCamera(bot.entity.position, bot.entity.yaw, bot.entity.pitch)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import fs from 'fs'
|
|||
import JSZip from 'jszip'
|
||||
import { proxy, subscribe } from 'valtio'
|
||||
import { WorldRendererThree } from 'renderer/viewer/lib/worldrendererThree'
|
||||
import { armorTextures } from 'renderer/viewer/lib/entity/armorModels'
|
||||
import { collectFilesToCopy, copyFilesAsyncWithProgress, mkdirRecursive, removeFileRecursiveAsync } from './browserfs'
|
||||
import { setLoadingScreenStatus } from './appStatus'
|
||||
import { showNotification } from './react/NotificationProvider'
|
||||
|
|
@ -203,7 +204,7 @@ const getFilesMapFromDir = async (dir: string) => {
|
|||
return files
|
||||
}
|
||||
|
||||
export const getResourcepackTiles = async (type: 'blocks' | 'items', existingTextures: string[]) => {
|
||||
export const getResourcepackTiles = async (type: 'blocks' | 'items' | 'armor', existingTextures: string[]) => {
|
||||
const basePath = await getActiveResourcepackBasePath()
|
||||
if (!basePath) return
|
||||
let firstTextureSize: number | undefined
|
||||
|
|
@ -212,11 +213,25 @@ export const getResourcepackTiles = async (type: 'blocks' | 'items', existingTex
|
|||
setLoadingScreenStatus(`Generating atlas texture for ${type}`)
|
||||
}
|
||||
const textures = {} as Record<string, HTMLImageElement>
|
||||
let path
|
||||
switch (type) {
|
||||
case 'blocks':
|
||||
path = 'block'
|
||||
break
|
||||
case 'items':
|
||||
path = 'item'
|
||||
break
|
||||
case 'armor':
|
||||
path = 'models/armor'
|
||||
break
|
||||
default:
|
||||
throw new Error('Invalid type')
|
||||
}
|
||||
for (const namespace of namespaces) {
|
||||
const texturesCommonBasePath = `${basePath}/assets/${namespace}/textures`
|
||||
const isMinecraftNamespace = namespace === 'minecraft'
|
||||
let texturesBasePath = `${texturesCommonBasePath}/${type === 'blocks' ? 'block' : 'item'}`
|
||||
const texturesBasePathAlt = `${texturesCommonBasePath}/${type === 'blocks' ? 'blocks' : 'items'}`
|
||||
let texturesBasePath = `${texturesCommonBasePath}/${path}`
|
||||
const texturesBasePathAlt = `${texturesCommonBasePath}/${path}s`
|
||||
if (!(await existsAsync(texturesBasePath))) {
|
||||
if (await existsAsync(texturesBasePathAlt)) {
|
||||
texturesBasePath = texturesBasePathAlt
|
||||
|
|
@ -465,9 +480,11 @@ const repeatArr = (arr, i) => Array.from({ length: i }, () => arr)
|
|||
const updateTextures = async () => {
|
||||
const origBlocksFiles = Object.keys(viewer.world.sourceData.blocksAtlases.latest.textures)
|
||||
const origItemsFiles = Object.keys(viewer.world.sourceData.itemsAtlases.latest.textures)
|
||||
const origArmorFiles = Object.keys(armorTextures)
|
||||
const { usedTextures: extraBlockTextures = new Set<string>() } = await prepareBlockstatesAndModels() ?? {}
|
||||
const blocksData = await getResourcepackTiles('blocks', [...origBlocksFiles, ...extraBlockTextures])
|
||||
const itemsData = await getResourcepackTiles('items', origItemsFiles)
|
||||
const armorData = await getResourcepackTiles('armor', origArmorFiles)
|
||||
await updateAllReplacableTextures()
|
||||
viewer.world.customTextures = {}
|
||||
if (blocksData) {
|
||||
|
|
@ -482,8 +499,14 @@ const updateTextures = async () => {
|
|||
textures: itemsData.textures
|
||||
}
|
||||
}
|
||||
if (armorData) {
|
||||
viewer.world.customTextures.armor = {
|
||||
tileSize: armorData.firstTextureSize,
|
||||
textures: armorData.textures
|
||||
}
|
||||
}
|
||||
if (viewer.world.active) {
|
||||
await viewer.world.updateTexturesData()
|
||||
await viewer.world.updateAssetsData()
|
||||
if (viewer.world instanceof WorldRendererThree) {
|
||||
viewer.world.rerenderAllChunks?.()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { proxy } from 'valtio'
|
||||
import { proxy, useSnapshot } from 'valtio'
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
import { useMedia } from 'react-use'
|
||||
import { options, watchValue } from './optionsStorage'
|
||||
import { useScale } from './react/UIProvider'
|
||||
|
||||
export const currentScaling = proxy({
|
||||
scale: 1,
|
||||
|
|
@ -9,11 +11,11 @@ window.currentScaling = currentScaling
|
|||
|
||||
const setScale = () => {
|
||||
const scaleValues = [
|
||||
{ maxWidth: 971, maxHeight: null, scale: 2 },
|
||||
{ maxWidth: 980, maxHeight: null, scale: 2 },
|
||||
{ maxWidth: null, maxHeight: 390, scale: 1.5 }, // todo allow to set the scaling at 360-400 (dynamic scaling setting)
|
||||
{ maxWidth: 590, maxHeight: null, scale: 1 },
|
||||
{ maxWidth: 620, maxHeight: null, scale: 1 },
|
||||
|
||||
{ maxWidth: 590, minHeight: 240, scale: 1.4 },
|
||||
{ maxWidth: 620, minHeight: 240, scale: 1.4 },
|
||||
]
|
||||
|
||||
const { innerWidth, innerHeight } = window
|
||||
|
|
@ -35,3 +37,7 @@ watchValue(currentScaling, (c) => {
|
|||
document.documentElement.style.setProperty('--guiScale', String(c.scale))
|
||||
})
|
||||
window.addEventListener('resize', setScale)
|
||||
|
||||
export const useAppScale = () => {
|
||||
return useSnapshot(currentScaling).scale
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,24 @@ module.exports.resolveSrv = function (hostname, callback) {
|
|||
Http.responseType = 'json'
|
||||
Http.send()
|
||||
|
||||
const minecraftServerHostname = hostname.startsWith('_minecraft._tcp.') ? hostname.slice('_minecraft._tcp.'.length) : null
|
||||
if (minecraftServerHostname) {
|
||||
Http.onerror = async function () {
|
||||
try {
|
||||
if (!globalThis.resolveDnsFallback) return
|
||||
const result = await globalThis.resolveDnsFallback(minecraftServerHostname)
|
||||
callback(null, result ? [{
|
||||
priority: 0,
|
||||
weight: 0,
|
||||
port: result.port,
|
||||
name: result.host
|
||||
}] : [])
|
||||
} catch (err) {
|
||||
callback(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Http.onload = function () {
|
||||
const { response } = Http
|
||||
if (response.Status === 3) {
|
||||
|
|
|
|||
|
|
@ -126,11 +126,7 @@ export const getWsProtocolStream = async (url: string) => {
|
|||
const CHANNEL_NAME = 'minecraft-web-client:data'
|
||||
|
||||
export const handleCustomChannel = async () => {
|
||||
await new Promise(resolve => {
|
||||
bot._client.once('login', resolve)
|
||||
})
|
||||
|
||||
bot._client.registerChannel(CHANNEL_NAME, ['string', []], true)
|
||||
bot._client.registerChannel(CHANNEL_NAME, ['string', []])
|
||||
const toCleanup = [] as Array<() => void>
|
||||
subscribe(activeModalStack, () => {
|
||||
if (activeModalStack.length === 1 && activeModalStack[0].reactType === 'main-menu') {
|
||||
|
|
@ -193,6 +189,11 @@ export const handleCustomChannel = async () => {
|
|||
}
|
||||
break
|
||||
}
|
||||
// todo
|
||||
case 'kick' as any: {
|
||||
bot.emit('end', (data as any).reason)
|
||||
break
|
||||
}
|
||||
case 'ui': {
|
||||
const { update } = data
|
||||
if (update.data === null) {
|
||||
|
|
|
|||
|
|
@ -96,6 +96,8 @@ export const watchOptionsAfterWorldViewInit = () => {
|
|||
watchValue(options, o => {
|
||||
if (!worldView) return
|
||||
worldView.keepChunksDistance = o.keepChunksDistance
|
||||
viewer.world.config.displayHand = o.handDisplay
|
||||
viewer.world.config.renderEars = o.renderEars
|
||||
viewer.world.config.showHand = o.showHand
|
||||
viewer.world.config.viewBobbing = o.viewBobbing
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { options } from './optionsStorage'
|
|||
import { itemBeingUsed } from './react/Crosshair'
|
||||
import { isCypress } from './standaloneUtils'
|
||||
import { displayClientChat } from './botUtils'
|
||||
import { playerState } from './mineflayer/playerState'
|
||||
|
||||
function getViewDirection (pitch, yaw) {
|
||||
const csPitch = Math.cos(pitch)
|
||||
|
|
@ -165,15 +166,39 @@ class WorldInteraction {
|
|||
}
|
||||
})
|
||||
|
||||
bot.on('blockBreakProgressObserved', (block: Block, destroyStage: number) => {
|
||||
if (this.cursorBlock?.position.equals(block.position)) {
|
||||
this.setBreakState(block, destroyStage)
|
||||
//@ts-expect-error mineflayer types are wrong
|
||||
bot.on('blockBreakProgressObserved', (block: Block, destroyStage: number, entity: Entity) => {
|
||||
if (this.cursorBlock?.position.equals(block.position) && entity.id === bot.entity.id) {
|
||||
if (!this.buttons[0]) {
|
||||
// Simulate left mouse button press
|
||||
this.buttons[0] = true
|
||||
this.update()
|
||||
}
|
||||
// this.setBreakState(block, destroyStage)
|
||||
}
|
||||
})
|
||||
|
||||
bot.on('blockBreakProgressEnd', (block: Block) => {
|
||||
if (this.currentBreakBlock?.block.position.equals(block.position)) {
|
||||
this.stopBreakAnimation()
|
||||
//@ts-expect-error mineflayer types are wrong
|
||||
bot.on('blockBreakProgressEnd', (block: Block, entity: Entity) => {
|
||||
if (this.currentBreakBlock?.block.position.equals(block.position) && entity.id === bot.entity.id) {
|
||||
if (!this.buttons[0]) {
|
||||
// Simulate left mouse button press
|
||||
this.buttons[0] = false
|
||||
this.update()
|
||||
}
|
||||
// this.stopBreakAnimation()
|
||||
}
|
||||
})
|
||||
|
||||
// Handle acknowledge_player_digging packet
|
||||
bot._client.on('acknowledge_player_digging', (data: { location: { x: number, y: number, z: number }, block: number, status: number, successful: boolean } | { sequenceId: number }) => {
|
||||
if ('location' in data && !data.successful) {
|
||||
const packetPos = new Vec3(data.location.x, data.location.y, data.location.z)
|
||||
if (this.cursorBlock?.position.equals(packetPos)) {
|
||||
this.buttons[0] = false
|
||||
this.update()
|
||||
this.stopBreakAnimation()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -320,6 +345,7 @@ class WorldInteraction {
|
|||
if (item) {
|
||||
customEvents.emit('activateItem', item, offhand ? 45 : bot.quickBarSlot, offhand)
|
||||
}
|
||||
playerState.startUsingItem()
|
||||
itemBeingUsed.name = (offhand ? bot.inventory.slots[45]?.name : bot.heldItem?.name) ?? null
|
||||
itemBeingUsed.hand = offhand ? 1 : 0
|
||||
}
|
||||
|
|
@ -331,6 +357,7 @@ class WorldInteraction {
|
|||
// "only foods and bow can be deactivated" - not true, shields also can be deactivated and client always sends this
|
||||
// if (bot.heldItem && (loadedData.foodsArray.map((f) => f.name).includes(bot.heldItem.name) || bot.heldItem.name === 'bow')) {
|
||||
bot.deactivateItem()
|
||||
playerState.stopUsingItem()
|
||||
// }
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue