Compare commits
153 commits
renderer-c
...
next
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
253e094c74 | ||
|
|
fef94f03fb | ||
|
|
e9f91f8ecd | ||
|
|
634df8d03d |
||
|
|
a88c8b5470 | ||
|
|
f51254d97a | ||
|
|
05cd560d6b | ||
|
|
b239636356 | ||
|
|
4f421ae45f | ||
|
|
3b94889bed |
||
|
|
636a7fdb54 |
||
|
|
c930365e32 | ||
|
|
852dd737ae | ||
|
|
06dc3cb033 | ||
|
|
c4097975bf | ||
|
|
1525fac2a1 | ||
|
|
f24cb49a87 | ||
|
|
0b1183f541 | ||
|
|
739a6fad24 | ||
|
|
7f7a14ac65 | ||
|
|
265d02d18d | ||
|
|
b2e36840b9 |
||
|
|
7043bf49f3 |
||
|
|
528d8f516b |
||
|
|
70534d8b5a |
||
|
|
9d54c70fb7 | ||
|
|
7e3ba8bece | ||
|
|
513201be87 | ||
|
|
cb82188272 | ||
|
|
d0d5234ba4 | ||
|
|
e81d608554 | ||
|
|
1f240d8c20 | ||
|
|
2a1746eb7a | ||
|
|
9718610131 | ||
|
|
8f62fbd4da | ||
|
|
bc2972fe99 | ||
|
|
a12c61bc6c |
||
|
|
6e0d54ea17 | ||
|
|
72e9e656cc | ||
|
|
4a5f2e799c | ||
|
|
a8fa3d47d1 | ||
|
|
9a84a7acfb | ||
|
|
d6eb1601e9 | ||
|
|
e1293b6cb3 | ||
|
|
cc4f705aea | ||
|
|
54c114a702 | ||
|
|
65575e2665 | ||
|
|
1ddaa79162 |
||
|
|
e2b141cca0 | ||
|
|
15e3325971 | ||
|
|
60fc5ef315 | ||
|
|
8827aab981 | ||
|
|
0a474e6780 | ||
|
|
cdd8c31a0e | ||
|
|
e7c358d3fc | ||
|
|
fb395041b9 | ||
|
|
353ba2ecb3 | ||
|
|
53cbff7699 | ||
|
|
caf4695637 | ||
|
|
167b49da08 |
||
|
|
d7bd26b6b5 | ||
|
|
d41527edc8 | ||
|
|
24ab260e8e | ||
|
|
c4b284b9b7 | ||
|
|
67855ae25a |
||
|
|
b9c8ade9bf | ||
|
|
4d7e3df859 |
||
|
|
a498778703 | ||
|
|
b6d4728c44 | ||
|
|
0dca8bbbe5 | ||
|
|
de9bfba3a8 | ||
|
|
45408476a5 | ||
|
|
c360115f60 | ||
|
|
a8635e9e2f |
||
|
|
5bd33a546a |
||
|
|
e9c7840dae | ||
|
|
52c0c75ccf | ||
|
|
b2f2d85e4f | ||
|
|
7a83a2a657 | ||
|
|
64da602294 | ||
|
|
a09cd7d3ed |
||
|
|
39aca1735e |
||
|
|
e9320c68d2 | ||
|
|
95cc0e6c74 | ||
|
|
826b24d9e2 |
||
|
|
16609aa010 | ||
|
|
09b0e2e493 | ||
|
|
c844b99cf2 | ||
|
|
089f2224e2 | ||
|
|
2f93c08b1e | ||
|
|
fa56d479b1 | ||
|
|
f489c5f477 |
||
|
|
45bc76d825 | ||
|
|
01567ea589 | ||
|
|
e8b0a34c0b | ||
|
|
5cfd301d10 | ||
|
|
cdd23bc6a6 | ||
|
|
4277c3a262 |
||
|
|
9f3d3f93fb |
||
|
|
7162d2f549 | ||
|
|
fcf987efe4 | ||
|
|
d112b01177 | ||
|
|
043e28ed97 | ||
|
|
08fbc67c31 | ||
|
|
3bf34a8781 | ||
|
|
3cc862b05d | ||
|
|
c913d63c46 | ||
|
|
3320f65b9c | ||
|
|
ed7c33ff9f | ||
|
|
9086435aee | ||
|
|
8a50412395 | ||
|
|
71257bdf13 | ||
|
|
7aea07f83a | ||
|
|
3bcf0f533a | ||
|
|
b1298cbe1f |
||
|
|
661892af7c | ||
|
|
c55827db96 | ||
|
|
a2711dbe6c | ||
|
|
f79e54f11d | ||
|
|
6f5239e1d8 | ||
|
|
13e145cc3a | ||
|
|
d4ff7de64e | ||
|
|
1310109c01 | ||
|
|
dc2c5a2d88 | ||
|
|
31b91e5a33 |
||
|
|
f2a11d0a73 |
||
|
|
6eae7136ec | ||
|
|
34eecc166f | ||
|
|
fec887c28d | ||
|
|
369166e094 | ||
|
|
e161426caf | ||
|
|
0e4435ef91 | ||
|
|
3336680a0e | ||
|
|
83d783226f | ||
|
|
af5a0b2835 | ||
|
|
eedd9f1b8f | ||
|
|
0b1bc76327 | ||
|
|
b839bb8b9b | ||
|
|
3a7f267b5b | ||
|
|
2055579b72 | ||
|
|
1148378ce6 | ||
|
|
383e6c4d80 | ||
|
|
e9e144621f | ||
|
|
332bd4e0f3 |
||
|
|
32b19ab7af | ||
|
|
5221104980 | ||
|
|
7c8ccba2c1 | ||
|
|
fdeb78d96b | ||
|
|
5269ad21b5 |
||
|
|
f126f56844 | ||
|
|
1b20845ed5 | ||
|
|
f3ff4bef03 |
||
|
|
679c3775f7 |
160 changed files with 8494 additions and 2549 deletions
18
.cursor/rules/vars-usage.mdc
Normal file
18
.cursor/rules/vars-usage.mdc
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
---
|
||||||
|
description: Restricts usage of the global Mineflayer `bot` variable to only src/ files; prohibits usage in renderer/. Specifies correct usage of player state and appViewer globals.
|
||||||
|
globs: src/**/*.ts,renderer/**/*.ts
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
Ask AI
|
||||||
|
|
||||||
|
- The global variable `bot` refers to the Mineflayer bot instance.
|
||||||
|
- You may use `bot` directly in any file under the `src/` directory (e.g., `src/mineflayer/playerState.ts`).
|
||||||
|
- Do **not** use `bot` directly in any file under the `renderer/` directory or its subfolders (e.g., `renderer/viewer/three/worldrendererThree.ts`).
|
||||||
|
- In renderer code, all bot/player state and events must be accessed via explicit interfaces, state managers, or passed-in objects, never by referencing `bot` directly.
|
||||||
|
- In renderer code (such as in `WorldRendererThree`), use the `playerState` property (e.g., `worldRenderer.playerState.gameMode`) to access player state. The implementation for `playerState` lives in `src/mineflayer/playerState.ts`.
|
||||||
|
- In `src/` code, you may use the global variable `appViewer` from `src/appViewer.ts` directly. Do **not** import `appViewer` or use `window.appViewer`; use the global `appViewer` variable as-is.
|
||||||
|
- Some other global variables that can be used without window prefixes are listed in src/globals.d.ts
|
||||||
|
|
||||||
|
Rationale: This ensures a clean separation between the Mineflayer logic (server-side/game logic) and the renderer (client-side/view logic), making the renderer portable and testable, and maintains proper usage of global state.
|
||||||
|
|
||||||
|
For more general project contributing guides see CONTRIBUTING.md on like how to setup the project. Use pnpm tsc if needed to validate result with typechecking the whole project.
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
// ],
|
// ],
|
||||||
"@stylistic/arrow-spacing": "error",
|
"@stylistic/arrow-spacing": "error",
|
||||||
"@stylistic/block-spacing": "error",
|
"@stylistic/block-spacing": "error",
|
||||||
|
"@typescript-eslint/no-this-alias": "off",
|
||||||
"@stylistic/brace-style": [
|
"@stylistic/brace-style": [
|
||||||
"error",
|
"error",
|
||||||
"1tbs",
|
"1tbs",
|
||||||
|
|
|
||||||
2
.github/workflows/benchmark.yml
vendored
2
.github/workflows/benchmark.yml
vendored
|
|
@ -26,7 +26,7 @@ jobs:
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 22
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- name: Move Cypress to dependencies
|
- name: Move Cypress to dependencies
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
2
.github/workflows/build-single-file.yml
vendored
2
.github/workflows/build-single-file.yml
vendored
|
|
@ -23,6 +23,8 @@ jobs:
|
||||||
|
|
||||||
- name: Build single-file version - minecraft.html
|
- name: Build single-file version - minecraft.html
|
||||||
run: pnpm build-single-file && mv dist/single/index.html minecraft.html
|
run: pnpm build-single-file && mv dist/single/index.html minecraft.html
|
||||||
|
env:
|
||||||
|
LOCAL_CONFIG_FILE: config.mcraft-only.json
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|
|
||||||
2
.github/workflows/build-zip.yml
vendored
2
.github/workflows/build-zip.yml
vendored
|
|
@ -23,6 +23,8 @@ jobs:
|
||||||
|
|
||||||
- name: Build project
|
- name: Build project
|
||||||
run: pnpm build
|
run: pnpm build
|
||||||
|
env:
|
||||||
|
LOCAL_CONFIG_FILE: config.mcraft-only.json
|
||||||
|
|
||||||
- name: Bundle server.js
|
- name: Bundle server.js
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -33,7 +33,7 @@ jobs:
|
||||||
cd package
|
cd package
|
||||||
zip -r ../self-host.zip .
|
zip -r ../self-host.zip .
|
||||||
- run: pnpm build-playground
|
- run: pnpm build-playground
|
||||||
- run: pnpm build-storybook
|
# - run: pnpm build-storybook
|
||||||
- run: pnpm test-unit
|
- run: pnpm test-unit
|
||||||
- run: pnpm lint
|
- run: pnpm lint
|
||||||
|
|
||||||
|
|
|
||||||
2
.github/workflows/next-deploy.yml
vendored
2
.github/workflows/next-deploy.yml
vendored
|
|
@ -36,7 +36,7 @@ jobs:
|
||||||
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
|
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
|
||||||
env:
|
env:
|
||||||
CONFIG_JSON_SOURCE: BUNDLED
|
CONFIG_JSON_SOURCE: BUNDLED
|
||||||
- run: pnpm build-storybook
|
LOCAL_CONFIG_FILE: config.mcraft-only.json
|
||||||
- name: Copy playground files
|
- name: Copy playground files
|
||||||
run: |
|
run: |
|
||||||
mkdir -p .vercel/output/static/playground
|
mkdir -p .vercel/output/static/playground
|
||||||
|
|
|
||||||
2
.github/workflows/preview.yml
vendored
2
.github/workflows/preview.yml
vendored
|
|
@ -78,7 +78,7 @@ jobs:
|
||||||
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
|
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
|
||||||
env:
|
env:
|
||||||
CONFIG_JSON_SOURCE: BUNDLED
|
CONFIG_JSON_SOURCE: BUNDLED
|
||||||
- run: pnpm build-storybook
|
LOCAL_CONFIG_FILE: config.mcraft-only.json
|
||||||
- name: Copy playground files
|
- name: Copy playground files
|
||||||
run: |
|
run: |
|
||||||
mkdir -p .vercel/output/static/playground
|
mkdir -p .vercel/output/static/playground
|
||||||
|
|
|
||||||
20
.github/workflows/release.yml
vendored
20
.github/workflows/release.yml
vendored
|
|
@ -34,7 +34,7 @@ jobs:
|
||||||
- run: vercel build --token=${{ secrets.VERCEL_TOKEN }} --prod
|
- run: vercel build --token=${{ secrets.VERCEL_TOKEN }} --prod
|
||||||
env:
|
env:
|
||||||
CONFIG_JSON_SOURCE: BUNDLED
|
CONFIG_JSON_SOURCE: BUNDLED
|
||||||
- run: pnpm build-storybook
|
LOCAL_CONFIG_FILE: config.mcraft-only.json
|
||||||
- name: Copy playground files
|
- name: Copy playground files
|
||||||
run: |
|
run: |
|
||||||
mkdir -p .vercel/output/static/playground
|
mkdir -p .vercel/output/static/playground
|
||||||
|
|
@ -48,12 +48,26 @@ jobs:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
publish_dir: .vercel/output/static
|
publish_dir: .vercel/output/static
|
||||||
force_orphan: true
|
force_orphan: true
|
||||||
|
|
||||||
|
# Create CNAME file for custom domain
|
||||||
|
- name: Create CNAME file
|
||||||
|
run: echo "github.mcraft.fun" > .vercel/output/static/CNAME
|
||||||
|
|
||||||
|
- name: Deploy to mwc-mcraft-pages repository
|
||||||
|
uses: peaceiris/actions-gh-pages@v3
|
||||||
|
with:
|
||||||
|
personal_token: ${{ secrets.MCW_MCRAFT_PAGE_DEPLOY_TOKEN }}
|
||||||
|
external_repository: ${{ github.repository_owner }}/mwc-mcraft-pages
|
||||||
|
publish_dir: .vercel/output/static
|
||||||
|
publish_branch: main
|
||||||
|
destination_dir: docs
|
||||||
|
force_orphan: true
|
||||||
|
|
||||||
- name: Change index.html title
|
- name: Change index.html title
|
||||||
run: |
|
run: |
|
||||||
# change <title>Minecraft Web Client</title> to <title>Minecraft Web Client — Free Online Browser Version</title>
|
# change <title>Minecraft Web Client</title> to <title>Minecraft Web Client — Free Online Browser Version</title>
|
||||||
sed -i 's/<title>Minecraft Web Client<\/title>/<title>Minecraft Web Client — Free Online Browser Version<\/title>/' .vercel/output/static/index.html
|
sed -i 's/<title>Minecraft Web Client<\/title>/<title>Minecraft Web Client — Free Online Browser Version<\/title>/' .vercel/output/static/index.html
|
||||||
|
|
||||||
- name: Deploy Project to Vercel
|
- name: Deploy Project to Vercel
|
||||||
uses: mathiasvr/command-output@v2.0.0
|
uses: mathiasvr/command-output@v2.0.0
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -19,5 +19,6 @@ generated
|
||||||
storybook-static
|
storybook-static
|
||||||
server-jar
|
server-jar
|
||||||
config.local.json
|
config.local.json
|
||||||
|
logs/
|
||||||
|
|
||||||
src/react/npmReactComponents.ts
|
src/react/npmReactComponents.ts
|
||||||
|
|
|
||||||
|
|
@ -177,8 +177,13 @@ New React components, improve UI (including mobile support).
|
||||||
|
|
||||||
## Updating Dependencies
|
## Updating Dependencies
|
||||||
|
|
||||||
1. Ensure mineflayer fork is up to date with the latest version of mineflayer original repo
|
1. Use `pnpm update-git-deps` to check and update git dependencies (like mineflayer fork, prismarine packages etc). The script will:
|
||||||
|
- Show which git dependencies have updates available
|
||||||
|
- Ask if you want to update them
|
||||||
|
- Skip dependencies listed in `pnpm.updateConfig.ignoreDependencies`
|
||||||
|
|
||||||
2. Update PrismarineJS dependencies to the latest version: `minecraft-data` (be sure to replace the version twice in the package.json), `mineflayer`, `minecraft-protocol`, `prismarine-block`, `prismarine-chunk`, `prismarine-item`, ...
|
2. Update PrismarineJS dependencies to the latest version: `minecraft-data` (be sure to replace the version twice in the package.json), `mineflayer`, `minecraft-protocol`, `prismarine-block`, `prismarine-chunk`, `prismarine-item`, ...
|
||||||
|
|
||||||
3. If `minecraft-protocol` patch fails, do this:
|
3. If `minecraft-protocol` patch fails, do this:
|
||||||
1. Remove the patch from `patchedDependencies` in `package.json`
|
1. Remove the patch from `patchedDependencies` in `package.json`
|
||||||
2. Run `pnpm patch minecraft-protocol`, open patch directory
|
2. Run `pnpm patch minecraft-protocol`, open patch directory
|
||||||
|
|
|
||||||
30
README.MD
30
README.MD
|
|
@ -6,9 +6,13 @@ Minecraft **clone** rewritten in TypeScript using the best modern web technologi
|
||||||
|
|
||||||
You can try this out at [mcraft.fun](https://mcraft.fun/), [pcm.gg](https://pcm.gg) (short link), [mcon.vercel.app](https://mcon.vercel.app/) or the GitHub pages deploy. Every commit from the default (`develop`) branch is deployed to [s.mcraft.fun](https://s.mcraft.fun/) and [s.pcm.gg](https://s.pcm.gg/) - so it's usually newer, but might be less stable.
|
You can try this out at [mcraft.fun](https://mcraft.fun/), [pcm.gg](https://pcm.gg) (short link), [mcon.vercel.app](https://mcon.vercel.app/) or the GitHub pages deploy. Every commit from the default (`develop`) branch is deployed to [s.mcraft.fun](https://s.mcraft.fun/) and [s.pcm.gg](https://s.pcm.gg/) - so it's usually newer, but might be less stable.
|
||||||
|
|
||||||
Don't confuse with [Eaglercraft](https://git.eaglercraft.rip/eaglercraft/eaglercraft-1.8) which is a REAL vanilla Minecraft Java edition port to the web (but with its own limitations). Eaglercraft is a fully playable solution, but this project is more in position of a "technical demo" to show how it's possible to build games for web at scale entirely with the JS ecosystem. Have fun!
|
> For Turkey/Russia use [ru.mcraft.fun](https://ru.mcraft.fun/) (since Cloudflare is blocked)
|
||||||
|
|
||||||
For building the project yourself / contributing, see [Development, Debugging & Contributing](#development-debugging--contributing). For reference at what and how web technologies / frameworks are used, see [TECH.md](./TECH.md).
|
Don't confuse with [Eaglercraft](https://git.eaglercraft.rip/eaglercraft/eaglercraft-1.8) which is a REAL vanilla Minecraft Java edition port to the web (but with its own limitations). Eaglercraft is a fully playable solution, meanwhile this project is aimed for *device-compatiiblity* and better performance so it feels portable, flexible and lightweight. It's also a very strong example on how to build true HTML games for the web at scale entirely with the JS ecosystem. Have fun!
|
||||||
|
|
||||||
|
For building the project yourself / contributing, see [Development, Debugging & Contributing](#development-debugging--contributing). For reference at what and how web technologies / frameworks are used, see [TECH.md](./TECH.md) (also for comparison with Eaglercraft).
|
||||||
|
|
||||||
|
> **Note**: You can deploy it on your own server in less than a minute using a one-liner script from [Minecraft Everywhere repo](https://github.com/zardoy/minecraft-everywhere)
|
||||||
|
|
||||||
### Big Features
|
### Big Features
|
||||||
|
|
||||||
|
|
@ -28,7 +32,7 @@ For building the project yourself / contributing, see [Development, Debugging &
|
||||||
- Support for custom rendering 3D engines. Modular architecture.
|
- Support for custom rendering 3D engines. Modular architecture.
|
||||||
- even even more!
|
- even even more!
|
||||||
|
|
||||||
All components that are in [Storybook](https://mcraft.fun/storybook) are published as npm module and can be used in other projects: [`minecraft-react`](https://npmjs.com/minecraft-react)
|
All components that are in [Storybook](https://minimap.mcraft.fun/storybook/) are published as npm module and can be used in other projects: [`minecraft-react`](https://npmjs.com/minecraft-react)
|
||||||
|
|
||||||
### Recommended Settings
|
### Recommended Settings
|
||||||
|
|
||||||
|
|
@ -40,15 +44,19 @@ All components that are in [Storybook](https://mcraft.fun/storybook) are publish
|
||||||
|
|
||||||
### Browser Notes
|
### Browser Notes
|
||||||
|
|
||||||
These browsers have issues with capturing pointer:
|
This project is tested with BrowserStack. Special thanks to [BrowserStack](https://www.browserstack.com/) for providing testing infrastructure!
|
||||||
|
|
||||||
|
Howerver, it's known that these browsers have issues:
|
||||||
|
|
||||||
**Opera Mini**: Disable *mouse gestures* in browsre settings to avoid opening new tab on right click hold
|
**Opera Mini**: Disable *mouse gestures* in browsre settings to avoid opening new tab on right click hold
|
||||||
|
|
||||||
**Vivaldi**: Disable Controls -> *Raw Input* in game settings if experiencing issues
|
**Vivaldi**: Disable Controls -> *Raw Input* in game settings if experiencing issues
|
||||||
|
|
||||||
### Versions Support
|
### Versions Support
|
||||||
|
|
||||||
Server versions 1.8 - 1.21.4 are supported.
|
Server versions 1.8 - 1.21.5 are supported.
|
||||||
First class versions (most of the features are tested on these versions):
|
First class versions (most of the features are tested on these versions):
|
||||||
|
|
||||||
- 1.19.4
|
- 1.19.4
|
||||||
- 1.21.4
|
- 1.21.4
|
||||||
|
|
||||||
|
|
@ -70,6 +78,8 @@ There is a builtin proxy, but you can also host your one! Just clone the repo, r
|
||||||
|
|
||||||
[](https://app.koyeb.com/deploy?name=minecraft-web-client&type=git&repository=zardoy%2Fminecraft-web-client&branch=next&builder=dockerfile&env%5B%5D=&ports=8080%3Bhttp%3B%2F)
|
[](https://app.koyeb.com/deploy?name=minecraft-web-client&type=git&repository=zardoy%2Fminecraft-web-client&branch=next&builder=dockerfile&env%5B%5D=&ports=8080%3Bhttp%3B%2F)
|
||||||
|
|
||||||
|
> **Note**: If you want to make **your own** Minecraft server accessible to web clients (without our proxies), you can use [mwc-proxy](https://github.com/zardoy/mwc-proxy) - a lightweight JS WebSocket proxy that runs on the same server as your Minecraft server, allowing players to connect directly via `wss://play.example.com`. `?client_mcraft` is added to the URL, so the proxy will know that it's this client.
|
||||||
|
|
||||||
Proxy servers are used to connect to Minecraft servers which use TCP protocol. When you connect connect to a server with a proxy, websocket connection is created between you (browser client) and the proxy server located in Europe, then the proxy connects to the Minecraft server and sends the data to the client (you) without any packet deserialization to avoid any additional delays. That said all the Minecraft protocol packets are processed by the client, right in your browser.
|
Proxy servers are used to connect to Minecraft servers which use TCP protocol. When you connect connect to a server with a proxy, websocket connection is created between you (browser client) and the proxy server located in Europe, then the proxy connects to the Minecraft server and sends the data to the client (you) without any packet deserialization to avoid any additional delays. That said all the Minecraft protocol packets are processed by the client, right in your browser.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
|
|
@ -117,12 +127,12 @@ There is world renderer playground ([link](https://mcon.vercel.app/playground/))
|
||||||
|
|
||||||
However, there are many things that can be done in online production version (like debugging actual source code). Also you can access some global variables in the console and there are a few useful examples:
|
However, there are many things that can be done in online production version (like debugging actual source code). Also you can access some global variables in the console and there are a few useful examples:
|
||||||
|
|
||||||
- `localStorage.debug = '*'` - Enables all debug messages! Warning: this will start all packets spam.
|
- If you type `debugToggle`, press enter in console - It will enables all debug messages! Warning: this will start all packets spam.
|
||||||
Instead I recommend setting `options.debugLogNotFrequentPackets`. Also you can use `debugTopPackets` (with JSON.stringify) to see what packets were received/sent by name
|
Instead I recommend setting `options.debugLogNotFrequentPackets`. Also you can use `debugTopPackets` (with JSON.stringify) to see what packets were received/sent by name
|
||||||
|
|
||||||
- `bot` - Mineflayer bot instance. See Mineflayer documentation for more.
|
- `bot` - Mineflayer bot instance. See Mineflayer documentation for more.
|
||||||
- `viewer` - Three.js viewer instance, basically does all the rendering.
|
- `world` - Three.js world instance, basically does all the rendering (part of renderer backend).
|
||||||
- `viewer.world.sectionObjects` - Object with all active chunk sections (geometries) in the world. Each chunk section is a Three.js mesh or group.
|
- `world.sectionObjects` - Object with all active chunk sections (geometries) in the world. Each chunk section is a Three.js mesh or group.
|
||||||
- `debugSceneChunks` - The same as above, but relative to current bot position (e.g. 0,0 is the current chunk).
|
- `debugSceneChunks` - The same as above, but relative to current bot position (e.g. 0,0 is the current chunk).
|
||||||
- `debugChangedOptions` - See what options are changed. Don't change options here.
|
- `debugChangedOptions` - See what options are changed. Don't change options here.
|
||||||
- `localServer`/`server` - Only for singleplayer mode/host. Flying Squid server instance, see it's documentation for more.
|
- `localServer`/`server` - Only for singleplayer mode/host. Flying Squid server instance, see it's documentation for more.
|
||||||
|
|
@ -131,7 +141,7 @@ Instead I recommend setting `options.debugLogNotFrequentPackets`. Also you can u
|
||||||
|
|
||||||
- `nbt.simplify(someNbt)` - Simplifies nbt data, so it's easier to read.
|
- `nbt.simplify(someNbt)` - Simplifies nbt data, so it's easier to read.
|
||||||
|
|
||||||
The most useful thing in devtools is the watch expression. You can add any expression there and it will be re-evaluated in real time. For example, you can add `viewer.camera.position` to see the camera position and so on.
|
The most useful thing in devtools is the watch expression. You can add any expression there and it will be re-evaluated in real time. For example, you can add `world.getCameraPosition()` to see the camera position and so on.
|
||||||
|
|
||||||
<img src="./docs-assets/watch-expr.png" alt="Watch expression" width="480"/>
|
<img src="./docs-assets/watch-expr.png" alt="Watch expression" width="480"/>
|
||||||
|
|
||||||
|
|
@ -168,6 +178,7 @@ Server specific:
|
||||||
- `?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.
|
- `?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.
|
- `?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.
|
- `?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.
|
||||||
|
- `?addPing=<ping>` - Add a latency to both sides of the connection. Useful for testing ping issues. For example `?addPing=100` will add 200ms to your ping.
|
||||||
|
|
||||||
Single player specific:
|
Single player specific:
|
||||||
|
|
||||||
|
|
@ -224,3 +235,4 @@ Only during development:
|
||||||
|
|
||||||
- [https://github.com/ClassiCube/ClassiCube](ClassiCube - Better C# Rewrite) [DEMO](https://www.classicube.net/server/play/?warned=true)
|
- [https://github.com/ClassiCube/ClassiCube](ClassiCube - Better C# Rewrite) [DEMO](https://www.classicube.net/server/play/?warned=true)
|
||||||
- [https://m.eaglercraft.com/](EaglerCraft) - Eaglercraft runnable on mobile (real Minecraft in the browser)
|
- [https://m.eaglercraft.com/](EaglerCraft) - Eaglercraft runnable on mobile (real Minecraft in the browser)
|
||||||
|
- [js-minecraft](https://github.com/LabyStudio/js-minecraft) - An insanely well done clone from the graphical side that inspired many features here
|
||||||
|
|
|
||||||
2
assets/customTextures/readme.md
Normal file
2
assets/customTextures/readme.md
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
here you can place custom textures for bundled files (blocks/items) e.g. blocks/stone.png
|
||||||
|
get file names from here (blocks/items) https://zardoy.github.io/mc-assets/
|
||||||
237
assets/debug-inputs.html
Normal file
237
assets/debug-inputs.html
Normal file
|
|
@ -0,0 +1,237 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Web Input Debugger</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
.key-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 60px);
|
||||||
|
gap: 5px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.key {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border: 2px solid #333;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
background: white;
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.key.pressed {
|
||||||
|
background: #90EE90;
|
||||||
|
}
|
||||||
|
.key .duration {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 2px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.key .count {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
right: 2px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.controls {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 10px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.wasd-container {
|
||||||
|
position: relative;
|
||||||
|
width: 190px;
|
||||||
|
height: 130px;
|
||||||
|
}
|
||||||
|
#KeyW {
|
||||||
|
position: absolute;
|
||||||
|
left: 65px;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
#KeyA {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 65px;
|
||||||
|
}
|
||||||
|
#KeyS {
|
||||||
|
position: absolute;
|
||||||
|
left: 65px;
|
||||||
|
top: 65px;
|
||||||
|
}
|
||||||
|
#KeyD {
|
||||||
|
position: absolute;
|
||||||
|
left: 130px;
|
||||||
|
top: 65px;
|
||||||
|
}
|
||||||
|
.space-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
#Space {
|
||||||
|
width: 190px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="controls">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="repeatMode"> Use keydown repeat mode (auto key-up after 150ms of no repeat)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wasd-container">
|
||||||
|
<div id="KeyW" class="key" data-code="KeyW">W</div>
|
||||||
|
<div id="KeyA" class="key" data-code="KeyA">A</div>
|
||||||
|
<div id="KeyS" class="key" data-code="KeyS">S</div>
|
||||||
|
<div id="KeyD" class="key" data-code="KeyD">D</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="key-container">
|
||||||
|
<div id="ControlLeft" class="key" data-code="ControlLeft">Ctrl</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-container">
|
||||||
|
<div id="Space" class="key" data-code="Space">Space</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const keys = {};
|
||||||
|
const keyStats = {};
|
||||||
|
const pressStartTimes = {};
|
||||||
|
const keyTimeouts = {};
|
||||||
|
|
||||||
|
function initKeyStats(code) {
|
||||||
|
if (!keyStats[code]) {
|
||||||
|
keyStats[code] = {
|
||||||
|
pressCount: 0,
|
||||||
|
duration: 0,
|
||||||
|
startTime: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateKeyVisuals(code) {
|
||||||
|
const element = document.getElementById(code);
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
const stats = keyStats[code];
|
||||||
|
if (keys[code]) {
|
||||||
|
element.classList.add('pressed');
|
||||||
|
const currentDuration = ((Date.now() - stats.startTime) / 1000).toFixed(1);
|
||||||
|
element.innerHTML = `${element.getAttribute('data-code').replace('Key', '').replace('Left', '')}<span class="duration">${currentDuration}s</span><span class="count">${stats.pressCount}</span>`;
|
||||||
|
} else {
|
||||||
|
element.classList.remove('pressed');
|
||||||
|
element.innerHTML = `${element.getAttribute('data-code').replace('Key', '').replace('Left', '')}<span class="count">${stats.pressCount}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function releaseKey(code) {
|
||||||
|
keys[code] = false;
|
||||||
|
if (pressStartTimes[code]) {
|
||||||
|
keyStats[code].duration += (Date.now() - pressStartTimes[code]) / 1000;
|
||||||
|
delete pressStartTimes[code];
|
||||||
|
}
|
||||||
|
updateKeyVisuals(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyDown(event) {
|
||||||
|
const code = event.code;
|
||||||
|
const isRepeatMode = document.getElementById('repeatMode').checked;
|
||||||
|
|
||||||
|
initKeyStats(code);
|
||||||
|
|
||||||
|
// Clear any existing timeout for this key
|
||||||
|
if (keyTimeouts[code]) {
|
||||||
|
clearTimeout(keyTimeouts[code]);
|
||||||
|
delete keyTimeouts[code];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRepeatMode) {
|
||||||
|
// In repeat mode, always handle the keydown
|
||||||
|
if (!keys[code] || event.repeat) {
|
||||||
|
keys[code] = true;
|
||||||
|
if (!event.repeat) {
|
||||||
|
// Only increment count on initial press, not repeats
|
||||||
|
keyStats[code].pressCount++;
|
||||||
|
keyStats[code].startTime = Date.now();
|
||||||
|
pressStartTimes[code] = Date.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set timeout to release key if no repeat events come
|
||||||
|
keyTimeouts[code] = setTimeout(() => {
|
||||||
|
releaseKey(code);
|
||||||
|
}, 150);
|
||||||
|
} else {
|
||||||
|
// In normal mode, only handle keydown if key is not already pressed
|
||||||
|
if (!keys[code]) {
|
||||||
|
keys[code] = true;
|
||||||
|
keyStats[code].pressCount++;
|
||||||
|
keyStats[code].startTime = Date.now();
|
||||||
|
pressStartTimes[code] = Date.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateKeyVisuals(code);
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyUp(event) {
|
||||||
|
const code = event.code;
|
||||||
|
const isRepeatMode = document.getElementById('repeatMode').checked;
|
||||||
|
|
||||||
|
if (!isRepeatMode) {
|
||||||
|
releaseKey(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize all monitored keys
|
||||||
|
const monitoredKeys = ['KeyW', 'KeyA', 'KeyS', 'KeyD', 'ControlLeft', 'Space'];
|
||||||
|
monitoredKeys.forEach(code => {
|
||||||
|
initKeyStats(code);
|
||||||
|
const element = document.getElementById(code);
|
||||||
|
if (element) {
|
||||||
|
element.innerHTML = `${element.getAttribute('data-code').replace('Key', '').replace('Left', '')}<span class="count">0</span>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start visual updates
|
||||||
|
setInterval(() => {
|
||||||
|
monitoredKeys.forEach(code => {
|
||||||
|
if (keys[code]) {
|
||||||
|
updateKeyVisuals(code);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
document.addEventListener('keyup', handleKeyUp);
|
||||||
|
|
||||||
|
// Handle mode changes
|
||||||
|
document.getElementById('repeatMode').addEventListener('change', () => {
|
||||||
|
// Release all keys when switching modes
|
||||||
|
monitoredKeys.forEach(code => {
|
||||||
|
if (keys[code]) {
|
||||||
|
releaseKey(code);
|
||||||
|
}
|
||||||
|
if (keyTimeouts[code]) {
|
||||||
|
clearTimeout(keyTimeouts[code]);
|
||||||
|
delete keyTimeouts[code];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
12
config.json
12
config.json
|
|
@ -3,15 +3,27 @@
|
||||||
"defaultHost": "<from-proxy>",
|
"defaultHost": "<from-proxy>",
|
||||||
"defaultProxy": "https://proxy.mcraft.fun",
|
"defaultProxy": "https://proxy.mcraft.fun",
|
||||||
"mapsProvider": "https://maps.mcraft.fun/",
|
"mapsProvider": "https://maps.mcraft.fun/",
|
||||||
|
"skinTexturesProxy": "",
|
||||||
"peerJsServer": "",
|
"peerJsServer": "",
|
||||||
"peerJsServerFallback": "https://p2p.mcraft.fun",
|
"peerJsServerFallback": "https://p2p.mcraft.fun",
|
||||||
"promoteServers": [
|
"promoteServers": [
|
||||||
{
|
{
|
||||||
"ip": "wss://play.mcraft.fun"
|
"ip": "wss://play.mcraft.fun"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"ip": "wss://play.webmc.fun",
|
||||||
|
"name": "WebMC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "wss://ws.fuchsmc.net"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"ip": "wss://play2.mcraft.fun"
|
"ip": "wss://play2.mcraft.fun"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"ip": "wss://play-creative.mcraft.fun",
|
||||||
|
"description": "Might be available soon, stay tuned!"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"ip": "kaboom.pw",
|
"ip": "kaboom.pw",
|
||||||
"version": "1.20.3",
|
"version": "1.20.3",
|
||||||
|
|
|
||||||
5
config.mcraft-only.json
Normal file
5
config.mcraft-only.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"alwaysReconnectButton": true,
|
||||||
|
"reportBugButtonWithReconnect": true,
|
||||||
|
"allowAutoConnect": true
|
||||||
|
}
|
||||||
13
experiments/three-item.html
Normal file
13
experiments/three-item.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Minecraft Item Viewer</title>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; overflow: hidden; }
|
||||||
|
canvas { display: block; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="module" src="./three-item.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
108
experiments/three-item.ts
Normal file
108
experiments/three-item.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
import * as THREE from 'three'
|
||||||
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||||
|
import itemsAtlas from 'mc-assets/dist/itemsAtlasLegacy.png'
|
||||||
|
import { createItemMeshFromCanvas, createItemMesh } from '../renderer/viewer/three/itemMesh'
|
||||||
|
|
||||||
|
// Create scene, camera and renderer
|
||||||
|
const scene = new THREE.Scene()
|
||||||
|
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
|
||||||
|
const renderer = new THREE.WebGLRenderer({ antialias: true })
|
||||||
|
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||||
|
document.body.appendChild(renderer.domElement)
|
||||||
|
|
||||||
|
// Setup camera and controls
|
||||||
|
camera.position.set(0, 0, 3)
|
||||||
|
const controls = new OrbitControls(camera, renderer.domElement)
|
||||||
|
controls.enableDamping = true
|
||||||
|
|
||||||
|
// Background and lights
|
||||||
|
scene.background = new THREE.Color(0x333333)
|
||||||
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7)
|
||||||
|
scene.add(ambientLight)
|
||||||
|
|
||||||
|
// Animation loop
|
||||||
|
function animate () {
|
||||||
|
requestAnimationFrame(animate)
|
||||||
|
controls.update()
|
||||||
|
renderer.render(scene, camera)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setupItemMesh () {
|
||||||
|
try {
|
||||||
|
const loader = new THREE.TextureLoader()
|
||||||
|
const atlasTexture = await loader.loadAsync(itemsAtlas)
|
||||||
|
|
||||||
|
// Pixel-art configuration
|
||||||
|
atlasTexture.magFilter = THREE.NearestFilter
|
||||||
|
atlasTexture.minFilter = THREE.NearestFilter
|
||||||
|
atlasTexture.generateMipmaps = false
|
||||||
|
atlasTexture.wrapS = atlasTexture.wrapT = THREE.ClampToEdgeWrapping
|
||||||
|
|
||||||
|
// Extract the tile at x=2, y=0 (16x16)
|
||||||
|
const tileSize = 16
|
||||||
|
const tileX = 2
|
||||||
|
const tileY = 0
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = tileSize
|
||||||
|
canvas.height = tileSize
|
||||||
|
const ctx = canvas.getContext('2d')!
|
||||||
|
|
||||||
|
ctx.imageSmoothingEnabled = false
|
||||||
|
ctx.drawImage(
|
||||||
|
atlasTexture.image,
|
||||||
|
tileX * tileSize,
|
||||||
|
tileY * tileSize,
|
||||||
|
tileSize,
|
||||||
|
tileSize,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
tileSize,
|
||||||
|
tileSize
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test both approaches - working manual extraction:
|
||||||
|
const meshOld = createItemMeshFromCanvas(canvas, { depth: 0.1 })
|
||||||
|
meshOld.position.x = -1
|
||||||
|
meshOld.rotation.x = -Math.PI / 12
|
||||||
|
meshOld.rotation.y = Math.PI / 12
|
||||||
|
scene.add(meshOld)
|
||||||
|
|
||||||
|
// And new unified function:
|
||||||
|
const atlasWidth = atlasTexture.image.width
|
||||||
|
const atlasHeight = atlasTexture.image.height
|
||||||
|
const u = (tileX * tileSize) / atlasWidth
|
||||||
|
const v = (tileY * tileSize) / atlasHeight
|
||||||
|
const sizeX = tileSize / atlasWidth
|
||||||
|
const sizeY = tileSize / atlasHeight
|
||||||
|
|
||||||
|
console.log('Debug texture coords:', {u, v, sizeX, sizeY, atlasWidth, atlasHeight})
|
||||||
|
|
||||||
|
const resultNew = createItemMesh(atlasTexture, {
|
||||||
|
u, v, sizeX, sizeY
|
||||||
|
}, {
|
||||||
|
faceCamera: false,
|
||||||
|
use3D: true,
|
||||||
|
depth: 0.1
|
||||||
|
})
|
||||||
|
|
||||||
|
resultNew.mesh.position.x = 1
|
||||||
|
resultNew.mesh.rotation.x = -Math.PI / 12
|
||||||
|
resultNew.mesh.rotation.y = Math.PI / 12
|
||||||
|
scene.add(resultNew.mesh)
|
||||||
|
|
||||||
|
animate()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to create item mesh:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle window resize
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
camera.aspect = window.innerWidth / window.innerHeight
|
||||||
|
camera.updateProjectionMatrix()
|
||||||
|
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start
|
||||||
|
setupItemMesh()
|
||||||
5
experiments/three-labels.html
Normal file
5
experiments/three-labels.html
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script type="module" src="three-labels.ts"></script>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; }
|
||||||
|
canvas { display: block; }
|
||||||
|
</style>
|
||||||
67
experiments/three-labels.ts
Normal file
67
experiments/three-labels.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
import * as THREE from 'three'
|
||||||
|
import { FirstPersonControls } from 'three/addons/controls/FirstPersonControls.js'
|
||||||
|
import { createWaypointSprite, WAYPOINT_CONFIG } from '../renderer/viewer/three/waypointSprite'
|
||||||
|
|
||||||
|
// Create scene, camera and renderer
|
||||||
|
const scene = new THREE.Scene()
|
||||||
|
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
|
||||||
|
const renderer = new THREE.WebGLRenderer({ antialias: true })
|
||||||
|
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||||
|
document.body.appendChild(renderer.domElement)
|
||||||
|
|
||||||
|
// Add FirstPersonControls
|
||||||
|
const controls = new FirstPersonControls(camera, renderer.domElement)
|
||||||
|
controls.lookSpeed = 0.1
|
||||||
|
controls.movementSpeed = 10
|
||||||
|
controls.lookVertical = true
|
||||||
|
controls.constrainVertical = true
|
||||||
|
controls.verticalMin = 0.1
|
||||||
|
controls.verticalMax = Math.PI - 0.1
|
||||||
|
|
||||||
|
// Position camera
|
||||||
|
camera.position.y = 1.6 // Typical eye height
|
||||||
|
camera.lookAt(0, 1.6, -1)
|
||||||
|
|
||||||
|
// Create a helper grid and axes
|
||||||
|
const grid = new THREE.GridHelper(20, 20)
|
||||||
|
scene.add(grid)
|
||||||
|
const axes = new THREE.AxesHelper(5)
|
||||||
|
scene.add(axes)
|
||||||
|
|
||||||
|
// Create waypoint sprite via utility
|
||||||
|
const waypoint = createWaypointSprite({
|
||||||
|
position: new THREE.Vector3(0, 0, -5),
|
||||||
|
color: 0xff0000,
|
||||||
|
label: 'Target',
|
||||||
|
})
|
||||||
|
scene.add(waypoint.group)
|
||||||
|
|
||||||
|
// Use built-in offscreen arrow from utils
|
||||||
|
waypoint.enableOffscreenArrow(true)
|
||||||
|
waypoint.setArrowParent(scene)
|
||||||
|
|
||||||
|
// Animation loop
|
||||||
|
function animate() {
|
||||||
|
requestAnimationFrame(animate)
|
||||||
|
|
||||||
|
const delta = Math.min(clock.getDelta(), 0.1)
|
||||||
|
controls.update(delta)
|
||||||
|
|
||||||
|
// Unified camera update (size, distance text, arrow, visibility)
|
||||||
|
const sizeVec = renderer.getSize(new THREE.Vector2())
|
||||||
|
waypoint.updateForCamera(camera.position, camera, sizeVec.width, sizeVec.height)
|
||||||
|
|
||||||
|
renderer.render(scene, camera)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle window resize
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
camera.aspect = window.innerWidth / window.innerHeight
|
||||||
|
camera.updateProjectionMatrix()
|
||||||
|
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add clock for controls
|
||||||
|
const clock = new THREE.Clock()
|
||||||
|
|
||||||
|
animate()
|
||||||
|
|
@ -1,101 +1,60 @@
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
import * as tweenJs from '@tweenjs/tween.js'
|
|
||||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
|
||||||
import * as THREE from 'three';
|
|
||||||
import Jimp from 'jimp';
|
|
||||||
|
|
||||||
|
// Create scene, camera and renderer
|
||||||
const scene = new THREE.Scene()
|
const scene = new THREE.Scene()
|
||||||
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
|
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
|
||||||
camera.position.set(0, 0, 5)
|
|
||||||
const renderer = new THREE.WebGLRenderer()
|
const renderer = new THREE.WebGLRenderer()
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||||
document.body.appendChild(renderer.domElement)
|
document.body.appendChild(renderer.domElement)
|
||||||
|
|
||||||
const controls = new OrbitControls(camera, renderer.domElement)
|
// Position camera
|
||||||
|
camera.position.z = 5
|
||||||
|
|
||||||
const geometry = new THREE.BoxGeometry(1, 1, 1)
|
// Create a canvas with some content
|
||||||
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
|
const canvas = document.createElement('canvas')
|
||||||
const cube = new THREE.Mesh(geometry, material)
|
canvas.width = 256
|
||||||
cube.position.set(0.5, 0.5, 0.5);
|
canvas.height = 256
|
||||||
const group = new THREE.Group()
|
const ctx = canvas.getContext('2d')
|
||||||
group.add(cube)
|
|
||||||
group.position.set(-0.5, -0.5, -0.5);
|
|
||||||
const outerGroup = new THREE.Group()
|
|
||||||
outerGroup.add(group)
|
|
||||||
outerGroup.scale.set(0.2, 0.2, 0.2)
|
|
||||||
outerGroup.position.set(1, 1, 0)
|
|
||||||
scene.add(outerGroup)
|
|
||||||
|
|
||||||
// const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshBasicMaterial({ color: 0x00_00_ff, transparent: true, opacity: 0.5 }))
|
scene.background = new THREE.Color(0x444444)
|
||||||
// mesh.position.set(0.5, 1, 0.5)
|
|
||||||
// const group = new THREE.Group()
|
|
||||||
// group.add(mesh)
|
|
||||||
// group.position.set(-0.5, -1, -0.5)
|
|
||||||
// const outerGroup = new THREE.Group()
|
|
||||||
// outerGroup.add(group)
|
|
||||||
// // outerGroup.position.set(this.camera.position.x, this.camera.position.y, this.camera.position.z)
|
|
||||||
// scene.add(outerGroup)
|
|
||||||
|
|
||||||
new tweenJs.Tween(group.rotation).to({ z: THREE.MathUtils.degToRad(90) }, 1000).yoyo(true).repeat(Infinity).start()
|
// Draw something on the canvas
|
||||||
|
ctx.fillStyle = '#444444'
|
||||||
|
// ctx.fillRect(0, 0, 256, 256)
|
||||||
|
ctx.fillStyle = 'red'
|
||||||
|
ctx.font = '48px Arial'
|
||||||
|
ctx.textAlign = 'center'
|
||||||
|
ctx.textBaseline = 'middle'
|
||||||
|
ctx.fillText('Hello!', 128, 128)
|
||||||
|
|
||||||
const tweenGroup = new tweenJs.Group()
|
// Create bitmap and texture
|
||||||
function animate () {
|
async function createTexturedBox() {
|
||||||
tweenGroup.update()
|
const canvas2 = new OffscreenCanvas(256, 256)
|
||||||
requestAnimationFrame(animate)
|
const ctx2 = canvas2.getContext('2d')!
|
||||||
// cube.rotation.x += 0.01
|
ctx2.drawImage(canvas, 0, 0)
|
||||||
// cube.rotation.y += 0.01
|
const texture = new THREE.Texture(canvas2)
|
||||||
renderer.render(scene, camera)
|
texture.magFilter = THREE.NearestFilter
|
||||||
|
texture.minFilter = THREE.NearestFilter
|
||||||
|
texture.needsUpdate = true
|
||||||
|
texture.flipY = false
|
||||||
|
|
||||||
|
// Create box with texture
|
||||||
|
const geometry = new THREE.BoxGeometry(2, 2, 2)
|
||||||
|
const material = new THREE.MeshBasicMaterial({
|
||||||
|
map: texture,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
premultipliedAlpha: false,
|
||||||
|
})
|
||||||
|
const cube = new THREE.Mesh(geometry, material)
|
||||||
|
scene.add(cube)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the textured box
|
||||||
|
createTexturedBox()
|
||||||
|
|
||||||
|
// Animation loop
|
||||||
|
function animate() {
|
||||||
|
requestAnimationFrame(animate)
|
||||||
|
renderer.render(scene, camera)
|
||||||
}
|
}
|
||||||
animate()
|
animate()
|
||||||
|
|
||||||
// let animation
|
|
||||||
|
|
||||||
window.animate = () => {
|
|
||||||
// new Tween.Tween(group.position).to({ y: group.position.y - 1}, 1000 * 0.35/2).yoyo(true).repeat(1).start()
|
|
||||||
new tweenJs.Tween(group.rotation, tweenGroup).to({ z: THREE.MathUtils.degToRad(90) }, 1000 * 0.35 / 2).yoyo(true).repeat(Infinity).start().onRepeat(() => {
|
|
||||||
console.log('done')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
window.stop = () => {
|
|
||||||
tweenGroup.removeAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function createGeometryFromImage() {
|
|
||||||
return new Promise<THREE.ShapeGeometry>((resolve, reject) => {
|
|
||||||
const img = new Image();
|
|
||||||
img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAABEElEQVQ4jWNkIAPw2Zv9J0cfXPOSvx/+L/n74T+HqsJ/JlI1T9u3i6H91B7ybdY+vgZuO1majV+fppFmPnuz/+ihy2dv9t/49Wm8mlECkV1FHh5FfPZm/1XXTGX4cechA4eKPMNVq1CGH7cfMBJ0rlxX+X8OVYX/xq9P/5frKifoZ0Z0AwS8HRkYGBgYvt+8xyDXUUbQZgwJPnuz/+wq8gw/7zxk+PXsFUFno0h6mon+l5fgZFhwnYmBTUqMgYGBgaAhLMiaHQyFGOZvf8Lw49FXRgYGhv8MDAwwg/7jMoQFFury/C8Y5m9/wnADohnZVryJhoWBARJ9Cw69gtmMAgiFAcuvZ68Yfj17hU8NXgAATdKfkzbQhBEAAAAASUVORK5CYII='
|
|
||||||
console.log('img.complete', img.complete)
|
|
||||||
img.onload = () => {
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.width = img.width;
|
|
||||||
canvas.height = img.height;
|
|
||||||
const context = canvas.getContext('2d');
|
|
||||||
context.drawImage(img, 0, 0, img.width, img.height);
|
|
||||||
const imgData = context.getImageData(0, 0, img.width, img.height);
|
|
||||||
|
|
||||||
const shape = new THREE.Shape();
|
|
||||||
for (let y = 0; y < img.height; y++) {
|
|
||||||
for (let x = 0; x < img.width; x++) {
|
|
||||||
const index = (y * img.width + x) * 4;
|
|
||||||
const alpha = imgData.data[index + 3];
|
|
||||||
if (alpha !== 0) {
|
|
||||||
shape.lineTo(x, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const geometry = new THREE.ShapeGeometry(shape);
|
|
||||||
resolve(geometry);
|
|
||||||
};
|
|
||||||
img.onerror = reject;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Usage:
|
|
||||||
const shapeGeomtry = createGeometryFromImage().then(geometry => {
|
|
||||||
const material = new THREE.MeshBasicMaterial({ color: 0xffffff });
|
|
||||||
const mesh = new THREE.Mesh(geometry, material);
|
|
||||||
scene.add(mesh);
|
|
||||||
})
|
|
||||||
|
|
|
||||||
23
index.html
23
index.html
|
|
@ -27,6 +27,7 @@
|
||||||
<div style="font-size: var(--font-size);color: rgb(176, 176, 176);margin-top: 3px;text-align: center" class="subtitle">A true Minecraft client in your browser!</div>
|
<div style="font-size: var(--font-size);color: rgb(176, 176, 176);margin-top: 3px;text-align: center" class="subtitle">A true Minecraft client in your browser!</div>
|
||||||
<!-- small text pre -->
|
<!-- small text pre -->
|
||||||
<div style="font-size: calc(var(--font-size) * 0.6);color: rgb(150, 150, 150);margin-top: 3px;text-align: center;white-space: pre-line;" class="advanced-info"></div>
|
<div style="font-size: calc(var(--font-size) * 0.6);color: rgb(150, 150, 150);margin-top: 3px;text-align: center;white-space: pre-line;" class="advanced-info"></div>
|
||||||
|
<div style="font-size: calc(var(--font-size) * 0.6);color: rgb(255, 100, 100);margin-top: 10px;text-align: center;display: none;" class="ios-warning">Only iOS 15+ is supported due to performance optimizations</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|
@ -36,6 +37,13 @@
|
||||||
if (!window.pageLoaded) {
|
if (!window.pageLoaded) {
|
||||||
document.documentElement.appendChild(loadingDivElem)
|
document.documentElement.appendChild(loadingDivElem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// iOS version detection
|
||||||
|
const getIOSVersion = () => {
|
||||||
|
const match = navigator.userAgent.match(/OS (\d+)_(\d+)_?(\d+)?/);
|
||||||
|
return match ? parseInt(match[1], 10) : null;
|
||||||
|
}
|
||||||
|
|
||||||
// load error handling
|
// load error handling
|
||||||
const onError = (errorOrMessage, log = false) => {
|
const onError = (errorOrMessage, log = false) => {
|
||||||
let message = errorOrMessage instanceof Error ? (errorOrMessage.stack ?? errorOrMessage.message) : errorOrMessage
|
let message = errorOrMessage instanceof Error ? (errorOrMessage.stack ?? errorOrMessage.message) : errorOrMessage
|
||||||
|
|
@ -46,12 +54,23 @@
|
||||||
const [errorMessage, ...errorStack] = message.split('\n')
|
const [errorMessage, ...errorStack] = message.split('\n')
|
||||||
document.querySelector('.initial-loader').querySelector('.subtitle').textContent = errorMessage
|
document.querySelector('.initial-loader').querySelector('.subtitle').textContent = errorMessage
|
||||||
document.querySelector('.initial-loader').querySelector('.advanced-info').textContent = errorStack.join('\n')
|
document.querySelector('.initial-loader').querySelector('.advanced-info').textContent = errorStack.join('\n')
|
||||||
|
|
||||||
|
// Show iOS warning if applicable
|
||||||
|
const iosVersion = getIOSVersion();
|
||||||
|
if (iosVersion !== null && iosVersion < 15) {
|
||||||
|
document.querySelector('.initial-loader').querySelector('.ios-warning').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
if (window.navigator.maxTouchPoints > 1) window.location.hash = '#dev' // show eruda
|
if (window.navigator.maxTouchPoints > 1) window.location.hash = '#dev' // show eruda
|
||||||
// unregister all sw
|
// unregister all sw
|
||||||
if (window.navigator.serviceWorker) {
|
if (window.navigator.serviceWorker && document.querySelector('.initial-loader').style.opacity !== 0) {
|
||||||
|
console.log('got worker')
|
||||||
window.navigator.serviceWorker.getRegistrations().then(registrations => {
|
window.navigator.serviceWorker.getRegistrations().then(registrations => {
|
||||||
registrations.forEach(registration => {
|
registrations.forEach(registration => {
|
||||||
registration.unregister()
|
console.log('got registration')
|
||||||
|
registration.unregister().then(() => {
|
||||||
|
console.log('worker unregistered')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
27
package.json
27
package.json
|
|
@ -7,6 +7,7 @@
|
||||||
"dev-proxy": "node server.js",
|
"dev-proxy": "node server.js",
|
||||||
"start": "run-p dev-proxy dev-rsbuild watch-mesher",
|
"start": "run-p dev-proxy dev-rsbuild watch-mesher",
|
||||||
"start2": "run-p dev-rsbuild watch-mesher",
|
"start2": "run-p dev-rsbuild watch-mesher",
|
||||||
|
"start-metrics": "ENABLE_METRICS=true rsbuild dev",
|
||||||
"build": "pnpm build-other-workers && rsbuild build",
|
"build": "pnpm build-other-workers && rsbuild build",
|
||||||
"build-analyze": "BUNDLE_ANALYZE=true rsbuild build && pnpm build-other-workers",
|
"build-analyze": "BUNDLE_ANALYZE=true rsbuild build && pnpm build-other-workers",
|
||||||
"build-single-file": "SINGLE_FILE_BUILD=true rsbuild build",
|
"build-single-file": "SINGLE_FILE_BUILD=true rsbuild build",
|
||||||
|
|
@ -31,7 +32,9 @@
|
||||||
"run-playground": "run-p watch-mesher watch-other-workers watch-playground",
|
"run-playground": "run-p watch-mesher watch-other-workers watch-playground",
|
||||||
"run-all": "run-p start run-playground",
|
"run-all": "run-p start run-playground",
|
||||||
"build-playground": "rsbuild build --config renderer/rsbuild.config.ts",
|
"build-playground": "rsbuild build --config renderer/rsbuild.config.ts",
|
||||||
"watch-playground": "rsbuild dev --config renderer/rsbuild.config.ts"
|
"watch-playground": "rsbuild dev --config renderer/rsbuild.config.ts",
|
||||||
|
"update-git-deps": "tsx scripts/updateGitDeps.ts",
|
||||||
|
"request-data": "tsx scripts/requestData.ts"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"prismarine",
|
"prismarine",
|
||||||
|
|
@ -51,6 +54,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dimaka/interface": "0.0.3-alpha.0",
|
"@dimaka/interface": "0.0.3-alpha.0",
|
||||||
"@floating-ui/react": "^0.26.1",
|
"@floating-ui/react": "^0.26.1",
|
||||||
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@nxg-org/mineflayer-auto-jump": "^0.7.18",
|
"@nxg-org/mineflayer-auto-jump": "^0.7.18",
|
||||||
"@nxg-org/mineflayer-tracker": "1.3.0",
|
"@nxg-org/mineflayer-tracker": "1.3.0",
|
||||||
"@react-oauth/google": "^0.12.1",
|
"@react-oauth/google": "^0.12.1",
|
||||||
|
|
@ -76,14 +80,14 @@
|
||||||
"esbuild-plugin-polyfill-node": "^0.3.0",
|
"esbuild-plugin-polyfill-node": "^0.3.0",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"filesize": "^10.0.12",
|
"filesize": "^10.0.12",
|
||||||
"flying-squid": "npm:@zardoy/flying-squid@^0.0.59",
|
"flying-squid": "npm:@zardoy/flying-squid@^0.0.104",
|
||||||
"framer-motion": "^12.9.2",
|
"framer-motion": "^12.9.2",
|
||||||
"fs-extra": "^11.1.1",
|
"fs-extra": "^11.1.1",
|
||||||
"google-drive-browserfs": "github:zardoy/browserfs#google-drive",
|
"google-drive-browserfs": "github:zardoy/browserfs#google-drive",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"mcraft-fun-mineflayer": "^0.1.23",
|
"mcraft-fun-mineflayer": "^0.1.23",
|
||||||
"minecraft-data": "3.89.0",
|
"minecraft-data": "3.98.0",
|
||||||
"minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master",
|
"minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master",
|
||||||
"mineflayer-item-map-downloader": "github:zardoy/mineflayer-item-map-downloader",
|
"mineflayer-item-map-downloader": "github:zardoy/mineflayer-item-map-downloader",
|
||||||
"mojangson": "^2.0.4",
|
"mojangson": "^2.0.4",
|
||||||
|
|
@ -150,11 +154,10 @@
|
||||||
"http-browserify": "^1.7.0",
|
"http-browserify": "^1.7.0",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
"https-browserify": "^1.0.0",
|
"https-browserify": "^1.0.0",
|
||||||
"mc-assets": "^0.2.59",
|
"mc-assets": "^0.2.62",
|
||||||
"minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next",
|
"minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next",
|
||||||
"mineflayer": "github:zardoy/mineflayer#gen-the-master",
|
"mineflayer": "github:zardoy/mineflayer#gen-the-master",
|
||||||
"mineflayer-mouse": "^0.1.10",
|
"mineflayer-mouse": "^0.1.21",
|
||||||
"mineflayer-pathfinder": "^2.4.4",
|
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"os-browserify": "^0.3.0",
|
"os-browserify": "^0.3.0",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
|
|
@ -194,6 +197,7 @@
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
"mineflayer": "github:zardoy/mineflayer#gen-the-master",
|
||||||
"@nxg-org/mineflayer-physics-util": "1.8.10",
|
"@nxg-org/mineflayer-physics-util": "1.8.10",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"vec3": "0.1.10",
|
"vec3": "0.1.10",
|
||||||
|
|
@ -201,7 +205,7 @@
|
||||||
"diamond-square": "github:zardoy/diamond-square",
|
"diamond-square": "github:zardoy/diamond-square",
|
||||||
"prismarine-block": "github:zardoy/prismarine-block#next-era",
|
"prismarine-block": "github:zardoy/prismarine-block#next-era",
|
||||||
"prismarine-world": "github:zardoy/prismarine-world#next-era",
|
"prismarine-world": "github:zardoy/prismarine-world#next-era",
|
||||||
"minecraft-data": "3.89.0",
|
"minecraft-data": "3.98.0",
|
||||||
"prismarine-provider-anvil": "github:zardoy/prismarine-provider-anvil#everything",
|
"prismarine-provider-anvil": "github:zardoy/prismarine-provider-anvil#everything",
|
||||||
"prismarine-physics": "github:zardoy/prismarine-physics",
|
"prismarine-physics": "github:zardoy/prismarine-physics",
|
||||||
"minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master",
|
"minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master",
|
||||||
|
|
@ -210,7 +214,10 @@
|
||||||
"prismarine-item": "latest"
|
"prismarine-item": "latest"
|
||||||
},
|
},
|
||||||
"updateConfig": {
|
"updateConfig": {
|
||||||
"ignoreDependencies": []
|
"ignoreDependencies": [
|
||||||
|
"browserfs",
|
||||||
|
"google-drive-browserfs"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"pixelarticons@1.8.1": "patches/pixelarticons@1.8.1.patch",
|
"pixelarticons@1.8.1": "patches/pixelarticons@1.8.1.patch",
|
||||||
|
|
@ -227,7 +234,9 @@
|
||||||
"cypress",
|
"cypress",
|
||||||
"esbuild",
|
"esbuild",
|
||||||
"fsevents"
|
"fsevents"
|
||||||
]
|
],
|
||||||
|
"ignorePatchFailures": false,
|
||||||
|
"allowUnusedPatches": false
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.8.0+sha512.0e82714d1b5b43c74610193cb20734897c1d00de89d0e18420aebc5977fa13d780a9cb05734624e81ebd81cc876cd464794850641c48b9544326b5622ca29971"
|
"packageManager": "pnpm@10.8.0+sha512.0e82714d1b5b43c74610193cb20734897c1d00de89d0e18420aebc5977fa13d780a9cb05734624e81ebd81cc876cd464794850641c48b9544326b5622ca29971"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,26 @@
|
||||||
diff --git a/README.md b/README.md
|
|
||||||
deleted file mode 100644
|
|
||||||
index fbcaa43667323a58b8110a4495938c2c6d2d6f83..0000000000000000000000000000000000000000
|
|
||||||
diff --git a/src/client/chat.js b/src/client/chat.js
|
diff --git a/src/client/chat.js b/src/client/chat.js
|
||||||
index f14269bea055d4329cd729271e7406ec4b344de7..00f5482eb6e3c911381ca9a728b1b4aae0d1d337 100644
|
index 0021870994fc59a82f0ac8aba0a65a8be43ef2f4..a53fceb843105ea2a1d88722b3fc7c3b43cb102a 100644
|
||||||
--- a/src/client/chat.js
|
--- a/src/client/chat.js
|
||||||
+++ b/src/client/chat.js
|
+++ b/src/client/chat.js
|
||||||
@@ -111,7 +111,7 @@ module.exports = function (client, options) {
|
@@ -116,7 +116,7 @@ module.exports = function (client, options) {
|
||||||
for (const player of packet.data) {
|
for (const player of packet.data) {
|
||||||
if (!player.chatSession) continue
|
if (player.chatSession) {
|
||||||
client._players[player.UUID] = {
|
client._players[player.uuid] = {
|
||||||
- publicKey: crypto.createPublicKey({ key: player.chatSession.publicKey.keyBytes, format: 'der', type: 'spki' }),
|
- publicKey: crypto.createPublicKey({ key: player.chatSession.publicKey.keyBytes, format: 'der', type: 'spki' }),
|
||||||
+ // publicKey: crypto.createPublicKey({ key: player.chatSession.publicKey.keyBytes, format: 'der', type: 'spki' }),
|
+ // publicKey: crypto.createPublicKey({ key: player.chatSession.publicKey.keyBytes, format: 'der', type: 'spki' }),
|
||||||
publicKeyDER: player.chatSession.publicKey.keyBytes,
|
publicKeyDER: player.chatSession.publicKey.keyBytes,
|
||||||
sessionUuid: player.chatSession.uuid
|
sessionUuid: player.chatSession.uuid
|
||||||
}
|
}
|
||||||
@@ -127,7 +127,7 @@ module.exports = function (client, options) {
|
@@ -126,7 +126,7 @@ module.exports = function (client, options) {
|
||||||
for (const player of packet.data) {
|
|
||||||
if (player.crypto) {
|
if (player.crypto) {
|
||||||
client._players[player.UUID] = {
|
client._players[player.uuid] = {
|
||||||
- publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }),
|
- publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }),
|
||||||
+ // publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }),
|
+ // publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }),
|
||||||
publicKeyDER: player.crypto.publicKey,
|
publicKeyDER: player.crypto.publicKey,
|
||||||
signature: player.crypto.signature,
|
signature: player.crypto.signature,
|
||||||
displayName: player.displayName || player.name
|
displayName: player.displayName || player.name
|
||||||
@@ -198,7 +198,7 @@ module.exports = function (client, options) {
|
@@ -196,7 +196,7 @@ module.exports = function (client, options) {
|
||||||
if (mcData.supportFeature('useChatSessions')) {
|
if (mcData.supportFeature('useChatSessions')) {
|
||||||
const tsDelta = BigInt(Date.now()) - packet.timestamp
|
const tsDelta = BigInt(Date.now()) - packet.timestamp
|
||||||
const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0
|
const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0
|
||||||
|
|
@ -31,8 +28,8 @@ index f14269bea055d4329cd729271e7406ec4b344de7..00f5482eb6e3c911381ca9a728b1b4aa
|
||||||
+ const verified = false && !packet.unsignedChatContent && updateAndValidateSession(packet.senderUuid, packet.plainMessage, packet.signature, packet.index, packet.previousMessages, packet.salt, packet.timestamp) && !expired
|
+ const verified = false && !packet.unsignedChatContent && updateAndValidateSession(packet.senderUuid, packet.plainMessage, packet.signature, packet.index, packet.previousMessages, packet.salt, packet.timestamp) && !expired
|
||||||
if (verified) client._signatureCache.push(packet.signature)
|
if (verified) client._signatureCache.push(packet.signature)
|
||||||
client.emit('playerChat', {
|
client.emit('playerChat', {
|
||||||
plainMessage: packet.plainMessage,
|
globalIndex: packet.globalIndex,
|
||||||
@@ -363,7 +363,7 @@ module.exports = function (client, options) {
|
@@ -362,7 +362,7 @@ module.exports = function (client, options) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,16 +38,16 @@ index f14269bea055d4329cd729271e7406ec4b344de7..00f5482eb6e3c911381ca9a728b1b4aa
|
||||||
options.timestamp = options.timestamp || BigInt(Date.now())
|
options.timestamp = options.timestamp || BigInt(Date.now())
|
||||||
options.salt = options.salt || 1n
|
options.salt = options.salt || 1n
|
||||||
|
|
||||||
@@ -405,7 +405,7 @@ module.exports = function (client, options) {
|
@@ -407,7 +407,7 @@ module.exports = function (client, options) {
|
||||||
message,
|
message,
|
||||||
timestamp: options.timestamp,
|
timestamp: options.timestamp,
|
||||||
salt: options.salt,
|
salt: options.salt,
|
||||||
- signature: (client.profileKeys && client._session) ? client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined,
|
- signature: (client.profileKeys && client._session) ? client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined,
|
||||||
+ signature: (client.profileKeys && client._session) ? await client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined,
|
+ signature: (client.profileKeys && client._session) ? await client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined,
|
||||||
offset: client._lastSeenMessages.pending,
|
offset: client._lastSeenMessages.pending,
|
||||||
|
checksum: computeChatChecksum(client._lastSeenMessages), // 1.21.5+
|
||||||
acknowledged
|
acknowledged
|
||||||
})
|
@@ -422,7 +422,7 @@ module.exports = function (client, options) {
|
||||||
@@ -419,7 +419,7 @@ module.exports = function (client, options) {
|
|
||||||
message,
|
message,
|
||||||
timestamp: options.timestamp,
|
timestamp: options.timestamp,
|
||||||
salt: options.salt,
|
salt: options.salt,
|
||||||
|
|
@ -60,7 +57,7 @@ index f14269bea055d4329cd729271e7406ec4b344de7..00f5482eb6e3c911381ca9a728b1b4aa
|
||||||
previousMessages: client._lastSeenMessages.map((e) => ({
|
previousMessages: client._lastSeenMessages.map((e) => ({
|
||||||
messageSender: e.sender,
|
messageSender: e.sender,
|
||||||
diff --git a/src/client/encrypt.js b/src/client/encrypt.js
|
diff --git a/src/client/encrypt.js b/src/client/encrypt.js
|
||||||
index b9d21bab9faccd5dbf1975fc423fc55c73e906c5..99ffd76527b410e3a393181beb260108f4c63536 100644
|
index 63cc2bd9615100bd2fd63dfe14c094aa6b8cd1c9..36df57d1196af9761d920fa285ac48f85410eaef 100644
|
||||||
--- a/src/client/encrypt.js
|
--- a/src/client/encrypt.js
|
||||||
+++ b/src/client/encrypt.js
|
+++ b/src/client/encrypt.js
|
||||||
@@ -25,7 +25,11 @@ module.exports = function (client, options) {
|
@@ -25,7 +25,11 @@ module.exports = function (client, options) {
|
||||||
|
|
@ -76,41 +73,24 @@ index b9d21bab9faccd5dbf1975fc423fc55c73e906c5..99ffd76527b410e3a393181beb260108
|
||||||
}
|
}
|
||||||
|
|
||||||
function onJoinServerResponse (err) {
|
function onJoinServerResponse (err) {
|
||||||
diff --git a/src/client/play.js b/src/client/play.js
|
diff --git a/src/client/pluginChannels.js b/src/client/pluginChannels.js
|
||||||
index 6e06dc15291b38e1eeeec8d7102187b2a23d70a3..f67454942db9276cbb9eab99c281cfe182cb8a1f 100644
|
index 671eb452f31e6b5fcd57d715f1009d010160c65f..7f69f511c8fb97d431ec5125c851b49be8e2ab76 100644
|
||||||
--- a/src/client/play.js
|
--- a/src/client/pluginChannels.js
|
||||||
+++ b/src/client/play.js
|
+++ b/src/client/pluginChannels.js
|
||||||
@@ -53,7 +53,7 @@ module.exports = function (client, options) {
|
@@ -57,7 +57,7 @@ module.exports = function (client, options) {
|
||||||
client.write('configuration_acknowledged', {})
|
try {
|
||||||
|
packet.data = proto.parsePacketBuffer(channel, packet.data).data
|
||||||
|
} catch (error) {
|
||||||
|
- client.emit('error', error)
|
||||||
|
+ client.emit('error', error, { customPayload: packet })
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
client.state = states.CONFIGURATION
|
|
||||||
- client.on('select_known_packs', () => {
|
|
||||||
+ client.once('select_known_packs', () => {
|
|
||||||
client.write('select_known_packs', { packs: [] })
|
|
||||||
})
|
|
||||||
// Server should send finish_configuration on its own right after sending the client a dimension codec
|
|
||||||
diff --git a/src/client.js b/src/client.js
|
diff --git a/src/client.js b/src/client.js
|
||||||
index 74749698f8cee05b5dc749c271544f78d06645b0..e77e0a3f41c1ee780c3abbd54b0801d248c2a07c 100644
|
index e369e77d055ba919e8f9da7b8e8b5dc879c74cf4..54bb9e6644388e9b6bd42b3012951875989cdf0c 100644
|
||||||
--- a/src/client.js
|
--- a/src/client.js
|
||||||
+++ b/src/client.js
|
+++ b/src/client.js
|
||||||
@@ -89,10 +89,12 @@ class Client extends EventEmitter {
|
@@ -111,7 +111,13 @@ class Client extends EventEmitter {
|
||||||
parsed.metadata.name = parsed.data.name
|
|
||||||
parsed.data = parsed.data.params
|
|
||||||
parsed.metadata.state = state
|
|
||||||
- debug('read packet ' + state + '.' + parsed.metadata.name)
|
|
||||||
- if (debug.enabled) {
|
|
||||||
- const s = JSON.stringify(parsed.data, null, 2)
|
|
||||||
- debug(s && s.length > 10000 ? parsed.data : s)
|
|
||||||
+ if (!globalThis.excludeCommunicationDebugEvents?.includes(parsed.metadata.name)) {
|
|
||||||
+ debug('read packet ' + state + '.' + parsed.metadata.name)
|
|
||||||
+ if (debug.enabled) {
|
|
||||||
+ const s = JSON.stringify(parsed.data, null, 2)
|
|
||||||
+ debug(s && s.length > 10000 ? parsed.data : s)
|
|
||||||
+ }
|
|
||||||
}
|
|
||||||
if (this._hasBundlePacket && parsed.metadata.name === 'bundle_delimiter') {
|
|
||||||
if (this._mcBundle.length) { // End bundle
|
|
||||||
@@ -110,7 +112,13 @@ class Client extends EventEmitter {
|
|
||||||
this._hasBundlePacket = false
|
this._hasBundlePacket = false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -125,7 +105,7 @@ index 74749698f8cee05b5dc749c271544f78d06645b0..e77e0a3f41c1ee780c3abbd54b0801d2
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -168,7 +176,10 @@ class Client extends EventEmitter {
|
@@ -169,7 +175,10 @@ class Client extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
const onFatalError = (err) => {
|
const onFatalError = (err) => {
|
||||||
|
|
@ -137,25 +117,21 @@ index 74749698f8cee05b5dc749c271544f78d06645b0..e77e0a3f41c1ee780c3abbd54b0801d2
|
||||||
endSocket()
|
endSocket()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,6 +208,8 @@ class Client extends EventEmitter {
|
@@ -198,6 +207,10 @@ class Client extends EventEmitter {
|
||||||
serializer -> framer -> socket -> splitter -> deserializer */
|
serializer -> framer -> socket -> splitter -> deserializer */
|
||||||
if (this.serializer) {
|
if (this.serializer) {
|
||||||
this.serializer.end()
|
this.serializer.end()
|
||||||
+ this.socket?.end()
|
+ setTimeout(() => {
|
||||||
+ this.socket?.emit('end')
|
+ this.socket?.end()
|
||||||
|
+ this.socket?.emit('end')
|
||||||
|
+ }, 2000) // allow the serializer to finish writing
|
||||||
} else {
|
} else {
|
||||||
if (this.socket) this.socket.end()
|
if (this.socket) this.socket.end()
|
||||||
}
|
}
|
||||||
@@ -238,8 +251,11 @@ class Client extends EventEmitter {
|
@@ -243,6 +256,7 @@ class Client extends EventEmitter {
|
||||||
|
debug('writing packet ' + this.state + '.' + name)
|
||||||
write (name, params) {
|
debug(params)
|
||||||
if (!this.serializer.writable) { return }
|
}
|
||||||
- debug('writing packet ' + this.state + '.' + name)
|
|
||||||
- debug(params)
|
|
||||||
+ if (!globalThis.excludeCommunicationDebugEvents?.includes(name)) {
|
|
||||||
+ debug(`[${this.state}] from ${this.isServer ? 'server' : 'client'}: ` + name)
|
|
||||||
+ debug(params)
|
|
||||||
+ }
|
|
||||||
+ this.emit('writePacket', name, params)
|
+ this.emit('writePacket', name, params)
|
||||||
this.serializer.write({ name, params })
|
this.serializer.write({ name, params })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
diff --git a/fonts/pixelart-icons-font.css b/fonts/pixelart-icons-font.css
|
diff --git a/fonts/pixelart-icons-font.css b/fonts/pixelart-icons-font.css
|
||||||
index 3b2ebe839370d96bf93ef5ca94a827f07e49378d..103ab4d6b9f3b5c9f41d1407e3cbf4ac392fbf41 100644
|
index 3b2ebe839370d96bf93ef5ca94a827f07e49378d..4f8d76be2ca6e4ddc43c68d0a6f0f69979165ab4 100644
|
||||||
--- a/fonts/pixelart-icons-font.css
|
--- a/fonts/pixelart-icons-font.css
|
||||||
+++ b/fonts/pixelart-icons-font.css
|
+++ b/fonts/pixelart-icons-font.css
|
||||||
@@ -1,16 +1,13 @@
|
@@ -1,16 +1,13 @@
|
||||||
|
|
@ -10,10 +10,11 @@ index 3b2ebe839370d96bf93ef5ca94a827f07e49378d..103ab4d6b9f3b5c9f41d1407e3cbf4ac
|
||||||
+ src:
|
+ src:
|
||||||
url("pixelart-icons-font.woff2?t=1711815892278") format("woff2"),
|
url("pixelart-icons-font.woff2?t=1711815892278") format("woff2"),
|
||||||
url("pixelart-icons-font.woff?t=1711815892278") format("woff"),
|
url("pixelart-icons-font.woff?t=1711815892278") format("woff"),
|
||||||
url('pixelart-icons-font.ttf?t=1711815892278') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
|
- url('pixelart-icons-font.ttf?t=1711815892278') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
|
||||||
- url('pixelart-icons-font.svg?t=1711815892278#pixelart-icons-font') format('svg'); /* iOS 4.1- */
|
- url('pixelart-icons-font.svg?t=1711815892278#pixelart-icons-font') format('svg'); /* iOS 4.1- */
|
||||||
|
+ url('pixelart-icons-font.ttf?t=1711815892278') format('truetype'); /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
|
||||||
}
|
}
|
||||||
|
|
||||||
[class^="pixelart-icons-font-"], [class*=" pixelart-icons-font-"] {
|
[class^="pixelart-icons-font-"], [class*=" pixelart-icons-font-"] {
|
||||||
font-family: 'pixelart-icons-font' !important;
|
font-family: 'pixelart-icons-font' !important;
|
||||||
- font-size:24px;
|
- font-size:24px;
|
||||||
|
|
|
||||||
563
pnpm-lock.yaml
generated
563
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -35,6 +35,10 @@ const buildOptions = {
|
||||||
define: {
|
define: {
|
||||||
'process.env.BROWSER': '"true"',
|
'process.env.BROWSER': '"true"',
|
||||||
},
|
},
|
||||||
|
loader: {
|
||||||
|
'.png': 'dataurl',
|
||||||
|
'.obj': 'text'
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
...mesherSharedPlugins,
|
...mesherSharedPlugins,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ function getAllMethods (obj) {
|
||||||
return [...methods] as string[]
|
return [...methods] as string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const delayedIterator = async <T> (arr: T[], delay: number, exec: (item: T, index: number) => void, chunkSize = 1) => {
|
export const delayedIterator = async <T> (arr: T[], delay: number, exec: (item: T, index: number) => Promise<void>, chunkSize = 1) => {
|
||||||
// if delay is 0 then don't use setTimeout
|
// if delay is 0 then don't use setTimeout
|
||||||
for (let i = 0; i < arr.length; i += chunkSize) {
|
for (let i = 0; i < arr.length; i += chunkSize) {
|
||||||
if (delay) {
|
if (delay) {
|
||||||
|
|
@ -74,6 +74,6 @@ export const delayedIterator = async <T> (arr: T[], delay: number, exec: (item:
|
||||||
setTimeout(resolve, delay)
|
setTimeout(resolve, delay)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
exec(arr[i], i)
|
await exec(arr[i], i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,12 +73,12 @@ export const appAndRendererSharedConfig = () => defineConfig({
|
||||||
})
|
})
|
||||||
|
|
||||||
export const rspackViewerConfig = (config, { appendPlugins, addRules, rspack }: ModifyRspackConfigUtils) => {
|
export const rspackViewerConfig = (config, { appendPlugins, addRules, rspack }: ModifyRspackConfigUtils) => {
|
||||||
appendPlugins(new rspack.NormalModuleReplacementPlugin(/data/, (resource) => {
|
appendPlugins(new rspack.NormalModuleReplacementPlugin(/data|prismarine-physics/, (resource) => {
|
||||||
let absolute: string
|
let absolute: string
|
||||||
const request = resource.request.replaceAll('\\', '/')
|
const request = resource.request.replaceAll('\\', '/')
|
||||||
absolute = path.join(resource.context, request).replaceAll('\\', '/')
|
absolute = path.join(resource.context, request).replaceAll('\\', '/')
|
||||||
if (request.includes('minecraft-data/data/pc/1.')) {
|
if (request.includes('minecraft-data/data/pc/1.') || request.includes('prismarine-physics')) {
|
||||||
console.log('Error: incompatible resource', request, resource.contextInfo.issuer)
|
console.log('Error: incompatible resource', request, 'from', resource.contextInfo.issuer)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
// throw new Error(`${resource.request} was requested by ${resource.contextInfo.issuer}`)
|
// throw new Error(`${resource.request} was requested by ${resource.contextInfo.issuer}`)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,27 @@
|
||||||
import { RendererReactiveState } from '../../src/appViewer'
|
import { proxy } from 'valtio'
|
||||||
|
import { NonReactiveState, RendererReactiveState } from '../../src/appViewer'
|
||||||
|
|
||||||
export const getDefaultRendererState = (): RendererReactiveState => {
|
export const getDefaultRendererState = (): {
|
||||||
|
reactive: RendererReactiveState
|
||||||
|
nonReactive: NonReactiveState
|
||||||
|
} => {
|
||||||
return {
|
return {
|
||||||
world: {
|
reactive: proxy({
|
||||||
chunksLoaded: new Set(),
|
world: {
|
||||||
heightmaps: new Map(),
|
chunksLoaded: new Set(),
|
||||||
chunksTotalNumber: 0,
|
heightmaps: new Map(),
|
||||||
allChunksLoaded: true,
|
allChunksLoaded: true,
|
||||||
mesherWork: false,
|
mesherWork: false,
|
||||||
intersectMedia: null
|
intersectMedia: null
|
||||||
},
|
},
|
||||||
renderer: '',
|
renderer: '',
|
||||||
preventEscapeMenu: false
|
preventEscapeMenu: false
|
||||||
|
}),
|
||||||
|
nonReactive: {
|
||||||
|
world: {
|
||||||
|
chunksLoaded: new Set(),
|
||||||
|
chunksTotalNumber: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,125 +1,87 @@
|
||||||
import { EventEmitter } from 'events'
|
|
||||||
import { Vec3 } from 'vec3'
|
|
||||||
import TypedEmitter from 'typed-emitter'
|
|
||||||
import { ItemSelector } from 'mc-assets/dist/itemDefinitions'
|
import { ItemSelector } from 'mc-assets/dist/itemDefinitions'
|
||||||
import { proxy, ref } from 'valtio'
|
import { GameMode, Team } from 'mineflayer'
|
||||||
import { GameMode } from 'mineflayer'
|
import { proxy } from 'valtio'
|
||||||
import { HandItemBlock } from '../three/holdingBlock'
|
import type { HandItemBlock } from '../three/holdingBlock'
|
||||||
|
|
||||||
export type MovementState = 'NOT_MOVING' | 'WALKING' | 'SPRINTING' | 'SNEAKING'
|
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 ItemSpecificContextProperties = Partial<Pick<ItemSelector['properties'], 'minecraft:using_item' | 'minecraft:use_duration' | 'minecraft:use_cycle' | 'minecraft:display_context'>>
|
||||||
|
export type CameraPerspective = 'first_person' | 'third_person_back' | 'third_person_front'
|
||||||
|
|
||||||
export type PlayerStateEvents = {
|
|
||||||
heldItemChanged: (item: HandItemBlock | undefined, isLeftHand: boolean) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BlockShape = { position: any; width: any; height: any; depth: any; }
|
export type BlockShape = { position: any; width: any; height: any; depth: any; }
|
||||||
export type BlocksShapes = BlockShape[]
|
export type BlocksShapes = BlockShape[]
|
||||||
|
|
||||||
export interface IPlayerState {
|
// edit src/mineflayer/playerState.ts for implementation of player state from mineflayer
|
||||||
getEyeHeight(): number
|
export const getInitialPlayerState = () => proxy({
|
||||||
getMovementState(): MovementState
|
playerSkin: undefined as string | undefined,
|
||||||
getVelocity(): Vec3
|
inWater: false,
|
||||||
isOnGround(): boolean
|
waterBreathing: false,
|
||||||
isSneaking(): boolean
|
backgroundColor: [0, 0, 0] as [number, number, number],
|
||||||
isFlying(): boolean
|
ambientLight: 0,
|
||||||
isSprinting (): boolean
|
directionalLight: 0,
|
||||||
getItemUsageTicks?(): number
|
eyeHeight: 0,
|
||||||
getPosition(): Vec3
|
gameMode: undefined as GameMode | undefined,
|
||||||
// isUsingItem?(): boolean
|
lookingAtBlock: undefined as {
|
||||||
getHeldItem?(isLeftHand: boolean): HandItemBlock | undefined
|
x: number
|
||||||
username?: string
|
y: number
|
||||||
onlineMode?: boolean
|
z: number
|
||||||
lightingDisabled?: boolean
|
face?: number
|
||||||
shouldHideHand?: boolean
|
shapes: BlocksShapes
|
||||||
|
} | undefined,
|
||||||
|
diggingBlock: undefined as {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
z: number
|
||||||
|
stage: number
|
||||||
|
face?: number
|
||||||
|
mergedShape: BlockShape | undefined
|
||||||
|
} | undefined,
|
||||||
|
movementState: 'NOT_MOVING' as MovementState,
|
||||||
|
onGround: true,
|
||||||
|
sneaking: false,
|
||||||
|
flying: false,
|
||||||
|
sprinting: false,
|
||||||
|
itemUsageTicks: 0,
|
||||||
|
username: '',
|
||||||
|
onlineMode: false,
|
||||||
|
lightingDisabled: false,
|
||||||
|
shouldHideHand: false,
|
||||||
|
heldItemMain: undefined as HandItemBlock | undefined,
|
||||||
|
heldItemOff: undefined as HandItemBlock | undefined,
|
||||||
|
perspective: 'first_person' as CameraPerspective,
|
||||||
|
onFire: false,
|
||||||
|
|
||||||
events: TypedEmitter<PlayerStateEvents>
|
cameraSpectatingEntity: undefined as number | undefined,
|
||||||
|
|
||||||
reactive: {
|
team: undefined as Team | undefined,
|
||||||
playerSkin: string | undefined
|
})
|
||||||
inWater: boolean
|
|
||||||
waterBreathing: boolean
|
export const getPlayerStateUtils = (reactive: PlayerStateReactive) => ({
|
||||||
backgroundColor: [number, number, number]
|
isSpectator () {
|
||||||
ambientLight: number
|
return reactive.gameMode === 'spectator'
|
||||||
directionalLight: number
|
},
|
||||||
gameMode?: GameMode
|
isSpectatingEntity () {
|
||||||
lookingAtBlock?: {
|
return reactive.cameraSpectatingEntity !== undefined && reactive.gameMode === 'spectator'
|
||||||
x: number
|
},
|
||||||
y: number
|
isThirdPerson () {
|
||||||
z: number
|
if ((this as PlayerStateUtils).isSpectatingEntity()) return false
|
||||||
face?: number
|
return reactive.perspective === 'third_person_back' || reactive.perspective === 'third_person_front'
|
||||||
shapes: BlocksShapes
|
}
|
||||||
}
|
})
|
||||||
diggingBlock?: {
|
|
||||||
x: number
|
export const getInitialPlayerStateRenderer = () => ({
|
||||||
y: number
|
reactive: getInitialPlayerState()
|
||||||
z: number
|
})
|
||||||
stage: number
|
|
||||||
face?: number
|
export type PlayerStateReactive = ReturnType<typeof getInitialPlayerState>
|
||||||
mergedShape?: BlockShape
|
export type PlayerStateUtils = ReturnType<typeof getPlayerStateUtils>
|
||||||
}
|
|
||||||
}
|
export type PlayerStateRenderer = PlayerStateReactive
|
||||||
}
|
|
||||||
|
export const getItemSelector = (playerState: PlayerStateRenderer, specificProperties: ItemSpecificContextProperties, item?: import('prismarine-item').Item) => {
|
||||||
export class BasePlayerState implements IPlayerState {
|
return {
|
||||||
reactive = proxy({
|
...specificProperties,
|
||||||
playerSkin: undefined as string | undefined,
|
'minecraft:date': new Date(),
|
||||||
inWater: false,
|
// "minecraft:context_dimension": bot.entityp,
|
||||||
waterBreathing: false,
|
// 'minecraft:time': bot.time.timeOfDay / 24_000,
|
||||||
backgroundColor: ref([0, 0, 0]) as [number, number, number],
|
|
||||||
ambientLight: 0,
|
|
||||||
directionalLight: 0,
|
|
||||||
})
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
getPosition (): Vec3 {
|
|
||||||
return new Vec3(0, 0, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// For testing purposes
|
|
||||||
setState (state: Partial<{
|
|
||||||
movementState: MovementState
|
|
||||||
velocity: Vec3
|
|
||||||
onGround: boolean
|
|
||||||
sneaking: boolean
|
|
||||||
flying: boolean
|
|
||||||
sprinting: boolean
|
|
||||||
}>) {
|
|
||||||
Object.assign(this, state)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
55
renderer/viewer/lib/createPlayerObject.ts
Normal file
55
renderer/viewer/lib/createPlayerObject.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { PlayerObject, PlayerAnimation } from 'skinview3d'
|
||||||
|
import * as THREE from 'three'
|
||||||
|
import { WalkingGeneralSwing } from '../three/entity/animations'
|
||||||
|
import { loadSkinImage, stevePngUrl } from './utils/skins'
|
||||||
|
|
||||||
|
export type PlayerObjectType = PlayerObject & {
|
||||||
|
animation?: PlayerAnimation
|
||||||
|
realPlayerUuid: string
|
||||||
|
realUsername: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPlayerObject (options: {
|
||||||
|
username?: string
|
||||||
|
uuid?: string
|
||||||
|
scale?: number
|
||||||
|
}): {
|
||||||
|
playerObject: PlayerObjectType
|
||||||
|
wrapper: THREE.Group
|
||||||
|
} {
|
||||||
|
const wrapper = new THREE.Group()
|
||||||
|
const playerObject = new PlayerObject() as PlayerObjectType
|
||||||
|
|
||||||
|
playerObject.realPlayerUuid = options.uuid ?? ''
|
||||||
|
playerObject.realUsername = options.username ?? ''
|
||||||
|
playerObject.position.set(0, 16, 0)
|
||||||
|
|
||||||
|
// fix issues with starfield
|
||||||
|
playerObject.traverse((obj) => {
|
||||||
|
if (obj instanceof THREE.Mesh && obj.material instanceof THREE.MeshStandardMaterial) {
|
||||||
|
obj.material.transparent = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
wrapper.add(playerObject as any)
|
||||||
|
const scale = options.scale ?? (1 / 16)
|
||||||
|
wrapper.scale.set(scale, scale, scale)
|
||||||
|
wrapper.rotation.set(0, Math.PI, 0)
|
||||||
|
|
||||||
|
// Set up animation
|
||||||
|
playerObject.animation = new WalkingGeneralSwing()
|
||||||
|
;(playerObject.animation as WalkingGeneralSwing).isMoving = false
|
||||||
|
playerObject.animation.update(playerObject, 0)
|
||||||
|
|
||||||
|
return { playerObject, wrapper }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const applySkinToPlayerObject = async (playerObject: PlayerObjectType, skinUrl: string) => {
|
||||||
|
return loadSkinImage(skinUrl || stevePngUrl).then(({ canvas }) => {
|
||||||
|
const skinTexture = new THREE.CanvasTexture(canvas)
|
||||||
|
skinTexture.magFilter = THREE.NearestFilter
|
||||||
|
skinTexture.minFilter = THREE.NearestFilter
|
||||||
|
skinTexture.needsUpdate = true
|
||||||
|
playerObject.skin.map = skinTexture as any
|
||||||
|
}).catch(console.error)
|
||||||
|
}
|
||||||
|
|
@ -9,11 +9,6 @@ import { makeTextureAtlas } from 'mc-assets/dist/atlasCreator'
|
||||||
import { proxy, ref } from 'valtio'
|
import { proxy, ref } from 'valtio'
|
||||||
import { getItemDefinition } from 'mc-assets/dist/itemDefinitions'
|
import { getItemDefinition } from 'mc-assets/dist/itemDefinitions'
|
||||||
|
|
||||||
export const activeGuiAtlas = proxy({
|
|
||||||
atlas: null as null | { json, image },
|
|
||||||
version: 0
|
|
||||||
})
|
|
||||||
|
|
||||||
export const getNonFullBlocksModels = () => {
|
export const getNonFullBlocksModels = () => {
|
||||||
let version = appViewer.resourcesManager.currentResources!.version ?? 'latest'
|
let version = appViewer.resourcesManager.currentResources!.version ?? 'latest'
|
||||||
if (versionToNumber(version) < versionToNumber('1.13')) version = '1.13'
|
if (versionToNumber(version) < versionToNumber('1.13')) version = '1.13'
|
||||||
|
|
@ -122,18 +117,18 @@ const RENDER_SIZE = 64
|
||||||
|
|
||||||
const generateItemsGui = async (models: Record<string, BlockModelMcAssets>, isItems = false) => {
|
const generateItemsGui = async (models: Record<string, BlockModelMcAssets>, isItems = false) => {
|
||||||
const { currentResources } = appViewer.resourcesManager
|
const { currentResources } = appViewer.resourcesManager
|
||||||
const img = await getLoadedImage(isItems ? currentResources!.itemsAtlasParser.latestImage : currentResources!.blocksAtlasParser.latestImage)
|
const imgBitmap = isItems ? currentResources!.itemsAtlasImage : currentResources!.blocksAtlasImage
|
||||||
const canvasTemp = document.createElement('canvas')
|
const canvasTemp = document.createElement('canvas')
|
||||||
canvasTemp.width = img.width
|
canvasTemp.width = imgBitmap.width
|
||||||
canvasTemp.height = img.height
|
canvasTemp.height = imgBitmap.height
|
||||||
canvasTemp.style.imageRendering = 'pixelated'
|
canvasTemp.style.imageRendering = 'pixelated'
|
||||||
const ctx = canvasTemp.getContext('2d')!
|
const ctx = canvasTemp.getContext('2d')!
|
||||||
ctx.imageSmoothingEnabled = false
|
ctx.imageSmoothingEnabled = false
|
||||||
ctx.drawImage(img, 0, 0)
|
ctx.drawImage(imgBitmap, 0, 0)
|
||||||
|
|
||||||
const atlasParser = isItems ? currentResources!.itemsAtlasParser : currentResources!.blocksAtlasParser
|
const atlasParser = isItems ? appViewer.resourcesManager.itemsAtlasParser : appViewer.resourcesManager.blocksAtlasParser
|
||||||
const textureAtlas = new TextureAtlas(
|
const textureAtlas = new TextureAtlas(
|
||||||
ctx.getImageData(0, 0, img.width, img.height),
|
ctx.getImageData(0, 0, imgBitmap.width, imgBitmap.height),
|
||||||
Object.fromEntries(Object.entries(atlasParser.atlas.latest.textures).map(([key, value]) => {
|
Object.fromEntries(Object.entries(atlasParser.atlas.latest.textures).map(([key, value]) => {
|
||||||
return [key, [
|
return [key, [
|
||||||
value.u,
|
value.u,
|
||||||
|
|
@ -243,6 +238,9 @@ const generateItemsGui = async (models: Record<string, BlockModelMcAssets>, isIt
|
||||||
return images
|
return images
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @mainThread
|
||||||
|
*/
|
||||||
const generateAtlas = async (images: Record<string, HTMLImageElement>) => {
|
const generateAtlas = async (images: Record<string, HTMLImageElement>) => {
|
||||||
const atlas = makeTextureAtlas({
|
const atlas = makeTextureAtlas({
|
||||||
input: Object.keys(images),
|
input: Object.keys(images),
|
||||||
|
|
@ -260,9 +258,9 @@ const generateAtlas = async (images: Record<string, HTMLImageElement>) => {
|
||||||
// a.download = 'blocks_atlas.png'
|
// a.download = 'blocks_atlas.png'
|
||||||
// a.click()
|
// a.click()
|
||||||
|
|
||||||
activeGuiAtlas.atlas = {
|
appViewer.resourcesManager.currentResources!.guiAtlas = {
|
||||||
json: atlas.json,
|
json: atlas.json,
|
||||||
image: ref(await getLoadedImage(atlas.canvas.toDataURL())),
|
image: await createImageBitmap(atlas.canvas),
|
||||||
}
|
}
|
||||||
|
|
||||||
return atlas
|
return atlas
|
||||||
|
|
@ -279,6 +277,6 @@ export const generateGuiAtlas = async () => {
|
||||||
const itemImages = await generateItemsGui(itemsModelsResolved, true)
|
const itemImages = await generateItemsGui(itemsModelsResolved, true)
|
||||||
console.timeEnd('generate items gui atlas')
|
console.timeEnd('generate items gui atlas')
|
||||||
await generateAtlas({ ...blockImages, ...itemImages })
|
await generateAtlas({ ...blockImages, ...itemImages })
|
||||||
activeGuiAtlas.version++
|
appViewer.resourcesManager.currentResources!.guiAtlasVersion++
|
||||||
// await generateAtlas(blockImages)
|
// await generateAtlas(blockImages)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ const handleMessage = data => {
|
||||||
|
|
||||||
if (data.type === 'mcData') {
|
if (data.type === 'mcData') {
|
||||||
globalVar.mcData = data.mcData
|
globalVar.mcData = data.mcData
|
||||||
|
globalVar.loadedData = data.mcData
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.config) {
|
if (data.config) {
|
||||||
|
|
@ -138,6 +139,7 @@ const handleMessage = data => {
|
||||||
dirtySections = new Map()
|
dirtySections = new Map()
|
||||||
// todo also remove cached
|
// todo also remove cached
|
||||||
globalVar.mcData = null
|
globalVar.mcData = null
|
||||||
|
globalVar.loadedData = null
|
||||||
allDataReady = false
|
allDataReady = false
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@ const getVec = (v: Vec3, dir: Vec3) => {
|
||||||
return v.plus(dir)
|
return v.plus(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, type: number, biome: string, water: boolean, attr: Record<string, any>, isRealWater: boolean) {
|
function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, type: number, biome: string, water: boolean, attr: MesherGeometryOutput, isRealWater: boolean) {
|
||||||
const heights: number[] = []
|
const heights: number[] = []
|
||||||
for (let z = -1; z <= 1; z++) {
|
for (let z = -1; z <= 1; z++) {
|
||||||
for (let x = -1; x <= 1; x++) {
|
for (let x = -1; x <= 1; x++) {
|
||||||
|
|
@ -192,13 +192,14 @@ function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, typ
|
||||||
|
|
||||||
for (const pos of corners) {
|
for (const pos of corners) {
|
||||||
const height = cornerHeights[pos[2] * 2 + pos[0]]
|
const height = cornerHeights[pos[2] * 2 + pos[0]]
|
||||||
attr.t_positions.push(
|
const OFFSET = 0.0001
|
||||||
(pos[0] ? 0.999 : 0.001) + (cursor.x & 15) - 8,
|
attr.t_positions!.push(
|
||||||
(pos[1] ? height - 0.001 : 0.001) + (cursor.y & 15) - 8,
|
(pos[0] ? 1 - OFFSET : OFFSET) + (cursor.x & 15) - 8,
|
||||||
(pos[2] ? 0.999 : 0.001) + (cursor.z & 15) - 8
|
(pos[1] ? height - OFFSET : OFFSET) + (cursor.y & 15) - 8,
|
||||||
|
(pos[2] ? 1 - OFFSET : OFFSET) + (cursor.z & 15) - 8
|
||||||
)
|
)
|
||||||
attr.t_normals.push(...dir)
|
attr.t_normals!.push(...dir)
|
||||||
attr.t_uvs.push(pos[3] * su + u, pos[4] * sv * (pos[1] ? 1 : height) + v)
|
attr.t_uvs!.push(pos[3] * su + u, pos[4] * sv * (pos[1] ? 1 : height) + v)
|
||||||
|
|
||||||
let cornerLightResult = baseLight
|
let cornerLightResult = baseLight
|
||||||
if (world.config.smoothLighting) {
|
if (world.config.smoothLighting) {
|
||||||
|
|
@ -223,7 +224,7 @@ function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, typ
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply light value to tint
|
// Apply light value to tint
|
||||||
attr.t_colors.push(tint[0] * cornerLightResult, tint[1] * cornerLightResult, tint[2] * cornerLightResult)
|
attr.t_colors!.push(tint[0] * cornerLightResult, tint[1] * cornerLightResult, tint[2] * cornerLightResult)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -335,7 +336,7 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO:
|
||||||
let localShift = null as any
|
let localShift = null as any
|
||||||
|
|
||||||
if (element.rotation && !needTiles) {
|
if (element.rotation && !needTiles) {
|
||||||
// todo do we support rescale?
|
// Rescale support for block model rotations
|
||||||
localMatrix = buildRotationMatrix(
|
localMatrix = buildRotationMatrix(
|
||||||
element.rotation.axis,
|
element.rotation.axis,
|
||||||
element.rotation.angle
|
element.rotation.angle
|
||||||
|
|
@ -348,6 +349,37 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO:
|
||||||
element.rotation.origin
|
element.rotation.origin
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Apply rescale if specified
|
||||||
|
if (element.rotation.rescale) {
|
||||||
|
const FIT_TO_BLOCK_SCALE_MULTIPLIER = 2 - Math.sqrt(2)
|
||||||
|
const angleRad = element.rotation.angle * Math.PI / 180
|
||||||
|
const scale = Math.abs(Math.sin(angleRad)) * FIT_TO_BLOCK_SCALE_MULTIPLIER
|
||||||
|
|
||||||
|
// Get axis vector components (1 for the rotation axis, 0 for others)
|
||||||
|
const axisX = element.rotation.axis === 'x' ? 1 : 0
|
||||||
|
const axisY = element.rotation.axis === 'y' ? 1 : 0
|
||||||
|
const axisZ = element.rotation.axis === 'z' ? 1 : 0
|
||||||
|
|
||||||
|
// Create scale matrix: scale = (1 - axisComponent) * scaleFactor + 1
|
||||||
|
const scaleMatrix = [
|
||||||
|
[(1 - axisX) * scale + 1, 0, 0],
|
||||||
|
[0, (1 - axisY) * scale + 1, 0],
|
||||||
|
[0, 0, (1 - axisZ) * scale + 1]
|
||||||
|
]
|
||||||
|
|
||||||
|
// Apply scaling to the transformation matrix
|
||||||
|
localMatrix = matmulmat3(localMatrix, scaleMatrix)
|
||||||
|
|
||||||
|
// Recalculate shift with the new matrix
|
||||||
|
localShift = vecsub3(
|
||||||
|
element.rotation.origin,
|
||||||
|
matmul3(
|
||||||
|
localMatrix,
|
||||||
|
element.rotation.origin
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const aos: number[] = []
|
const aos: number[] = []
|
||||||
|
|
@ -487,7 +519,7 @@ const isBlockWaterlogged = (block: Block) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
let unknownBlockModel: BlockModelPartsResolved
|
let unknownBlockModel: BlockModelPartsResolved
|
||||||
export function getSectionGeometry (sx, sy, sz, world: World) {
|
export function getSectionGeometry (sx: number, sy: number, sz: number, world: World) {
|
||||||
let delayedRender = [] as Array<() => void>
|
let delayedRender = [] as Array<() => void>
|
||||||
|
|
||||||
const attr: MesherGeometryOutput = {
|
const attr: MesherGeometryOutput = {
|
||||||
|
|
@ -510,7 +542,6 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
|
||||||
heads: {},
|
heads: {},
|
||||||
signs: {},
|
signs: {},
|
||||||
// isFull: true,
|
// isFull: true,
|
||||||
highestBlocks: new Map(),
|
|
||||||
hadErrors: false,
|
hadErrors: false,
|
||||||
blocksCount: 0
|
blocksCount: 0
|
||||||
}
|
}
|
||||||
|
|
@ -520,12 +551,6 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
|
||||||
for (cursor.z = sz; cursor.z < sz + 16; cursor.z++) {
|
for (cursor.z = sz; cursor.z < sz + 16; cursor.z++) {
|
||||||
for (cursor.x = sx; cursor.x < sx + 16; cursor.x++) {
|
for (cursor.x = sx; cursor.x < sx + 16; cursor.x++) {
|
||||||
let block = world.getBlock(cursor, blockProvider, attr)!
|
let block = world.getBlock(cursor, blockProvider, attr)!
|
||||||
if (!INVISIBLE_BLOCKS.has(block.name)) {
|
|
||||||
const highest = attr.highestBlocks.get(`${cursor.x},${cursor.z}`)
|
|
||||||
if (!highest || highest.y < cursor.y) {
|
|
||||||
attr.highestBlocks.set(`${cursor.x},${cursor.z}`, { y: cursor.y, stateId: block.stateId, biomeId: block.biome.id })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (INVISIBLE_BLOCKS.has(block.name)) continue
|
if (INVISIBLE_BLOCKS.has(block.name)) continue
|
||||||
if ((block.name.includes('_sign') || block.name === 'sign') && !world.config.disableSignsMapsSupport) {
|
if ((block.name.includes('_sign') || block.name === 'sign') && !world.config.disableSignsMapsSupport) {
|
||||||
const key = `${cursor.x},${cursor.y},${cursor.z}`
|
const key = `${cursor.x},${cursor.y},${cursor.z}`
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export const defaultMesherConfig = {
|
||||||
skyLight: 15,
|
skyLight: 15,
|
||||||
smoothLighting: true,
|
smoothLighting: true,
|
||||||
outputFormat: 'threeJs' as 'threeJs' | 'webgpu',
|
outputFormat: 'threeJs' as 'threeJs' | 'webgpu',
|
||||||
textureSize: 1024, // for testing
|
// textureSize: 1024, // for testing
|
||||||
debugModelVariant: undefined as undefined | number[],
|
debugModelVariant: undefined as undefined | number[],
|
||||||
clipWorldBelowY: undefined as undefined | number,
|
clipWorldBelowY: undefined as undefined | number,
|
||||||
disableSignsMapsSupport: false
|
disableSignsMapsSupport: false
|
||||||
|
|
@ -42,7 +42,6 @@ export type MesherGeometryOutput = {
|
||||||
heads: Record<string, any>,
|
heads: Record<string, any>,
|
||||||
signs: Record<string, any>,
|
signs: Record<string, any>,
|
||||||
// isFull: boolean
|
// isFull: boolean
|
||||||
highestBlocks: Map<string, HighestBlockInfo>
|
|
||||||
hadErrors: boolean
|
hadErrors: boolean
|
||||||
blocksCount: number
|
blocksCount: number
|
||||||
customBlockModels?: CustomBlockModels
|
customBlockModels?: CustomBlockModels
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
import { fromFormattedString } from '@xmcl/text-component'
|
|
||||||
|
|
||||||
export const formattedStringToSimpleString = (str) => {
|
|
||||||
const result = fromFormattedString(str)
|
|
||||||
str = result.text
|
|
||||||
// todo recursive
|
|
||||||
for (const extra of result.extra) {
|
|
||||||
str += extra.text
|
|
||||||
}
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +1,3 @@
|
||||||
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, highPriority = true): Promise<HTMLScriptElement> {
|
export const loadScript = async function (scriptSrc: string, highPriority = true): Promise<HTMLScriptElement> {
|
||||||
const existingScript = document.querySelector<HTMLScriptElement>(`script[src="${scriptSrc}"]`)
|
const existingScript = document.querySelector<HTMLScriptElement>(`script[src="${scriptSrc}"]`)
|
||||||
if (existingScript) {
|
if (existingScript) {
|
||||||
|
|
@ -49,3 +25,33 @@ export const loadScript = async function (scriptSrc: string, highPriority = true
|
||||||
document.head.appendChild(scriptElement)
|
document.head.appendChild(scriptElement)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const detectFullOffscreenCanvasSupport = () => {
|
||||||
|
if (typeof OffscreenCanvas === 'undefined') return false
|
||||||
|
try {
|
||||||
|
const canvas = new OffscreenCanvas(1, 1)
|
||||||
|
// Try to get a WebGL context - this will fail on iOS where only 2D is supported (iOS 16)
|
||||||
|
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl')
|
||||||
|
return gl !== null
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasFullOffscreenCanvasSupport = detectFullOffscreenCanvasSupport()
|
||||||
|
|
||||||
|
export const createCanvas = (width: number, height: number): OffscreenCanvas => {
|
||||||
|
if (hasFullOffscreenCanvasSupport) {
|
||||||
|
return new OffscreenCanvas(width, height)
|
||||||
|
}
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = width
|
||||||
|
canvas.height = height
|
||||||
|
return canvas as unknown as OffscreenCanvas // todo-low
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadImageFromUrl (imageUrl: string): Promise<ImageBitmap> {
|
||||||
|
const response = await fetch(imageUrl)
|
||||||
|
const blob = await response.blob()
|
||||||
|
return createImageBitmap(blob)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,7 @@
|
||||||
import { loadSkinToCanvas } from 'skinview-utils'
|
import { loadSkinToCanvas } from 'skinview-utils'
|
||||||
import * as THREE from 'three'
|
import { createCanvas, loadImageFromUrl } from '../utils'
|
||||||
import stevePng from 'mc-assets/dist/other-textures/latest/entity/player/wide/steve.png'
|
|
||||||
|
|
||||||
// eslint-disable-next-line unicorn/prefer-export-from
|
export { default as stevePngUrl } from 'mc-assets/dist/other-textures/latest/entity/player/wide/steve.png'
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
apiEnabled: true,
|
apiEnabled: true,
|
||||||
|
|
@ -52,13 +40,13 @@ export const parseSkinTexturesValue = (value: string) => {
|
||||||
return decodedData.textures?.SKIN?.url
|
return decodedData.textures?.SKIN?.url
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadSkinImage (skinUrl: string): Promise<{ canvas: HTMLCanvasElement, image: HTMLImageElement }> {
|
export async function loadSkinImage (skinUrl: string): Promise<{ canvas: OffscreenCanvas, image: ImageBitmap }> {
|
||||||
if (!skinUrl.startsWith('data:')) {
|
if (!skinUrl.startsWith('data:')) {
|
||||||
skinUrl = await fetchAndConvertBase64Skin(skinUrl.replace('http://', 'https://'))
|
skinUrl = await fetchAndConvertBase64Skin(skinUrl.replace('http://', 'https://'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const image = await loadImageFromUrl(skinUrl)
|
const image = await loadImageFromUrl(skinUrl)
|
||||||
const skinCanvas = document.createElement('canvas')
|
const skinCanvas = createCanvas(64, 64)
|
||||||
loadSkinToCanvas(skinCanvas, image)
|
loadSkinToCanvas(skinCanvas, image)
|
||||||
return { canvas: skinCanvas, image }
|
return { canvas: skinCanvas, image }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,20 @@
|
||||||
export function createWorkerProxy<T extends Record<string, (...args: any[]) => void>> (handlers: T, channel?: MessagePort): { __workerProxy: T } {
|
import { proxy, getVersion, subscribe } from 'valtio'
|
||||||
|
|
||||||
|
export function createWorkerProxy<T extends Record<string, (...args: any[]) => void | Promise<any>>> (handlers: T, channel?: MessagePort): { __workerProxy: T } {
|
||||||
const target = channel ?? globalThis
|
const target = channel ?? globalThis
|
||||||
target.addEventListener('message', (event: any) => {
|
target.addEventListener('message', (event: any) => {
|
||||||
const { type, args } = event.data
|
const { type, args, msgId } = event.data
|
||||||
if (handlers[type]) {
|
if (handlers[type]) {
|
||||||
handlers[type](...args)
|
const result = handlers[type](...args)
|
||||||
|
if (result instanceof Promise) {
|
||||||
|
void result.then((result) => {
|
||||||
|
target.postMessage({
|
||||||
|
type: 'result',
|
||||||
|
msgId,
|
||||||
|
args: [result]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return null as any
|
return null as any
|
||||||
|
|
@ -23,6 +34,7 @@ export function createWorkerProxy<T extends Record<string, (...args: any[]) => v
|
||||||
export const useWorkerProxy = <T extends { __workerProxy: Record<string, (...args: any[]) => void> }> (worker: Worker | MessagePort, autoTransfer = true): T['__workerProxy'] & {
|
export const useWorkerProxy = <T extends { __workerProxy: Record<string, (...args: any[]) => void> }> (worker: Worker | MessagePort, autoTransfer = true): T['__workerProxy'] & {
|
||||||
transfer: (...args: Transferable[]) => T['__workerProxy']
|
transfer: (...args: Transferable[]) => T['__workerProxy']
|
||||||
} => {
|
} => {
|
||||||
|
let messageId = 0
|
||||||
// in main thread
|
// in main thread
|
||||||
return new Proxy({} as any, {
|
return new Proxy({} as any, {
|
||||||
get (target, prop) {
|
get (target, prop) {
|
||||||
|
|
@ -41,11 +53,30 @@ export const useWorkerProxy = <T extends { __workerProxy: Record<string, (...arg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (...args: any[]) => {
|
return (...args: any[]) => {
|
||||||
const transfer = autoTransfer ? args.filter(arg => arg instanceof ArrayBuffer || arg instanceof MessagePort || arg instanceof ImageBitmap || arg instanceof OffscreenCanvas || arg instanceof ImageData) : []
|
const msgId = messageId++
|
||||||
|
const transfer = autoTransfer ? args.filter(arg => {
|
||||||
|
return arg instanceof ArrayBuffer || arg instanceof MessagePort
|
||||||
|
|| (typeof ImageBitmap !== 'undefined' && arg instanceof ImageBitmap)
|
||||||
|
|| (typeof OffscreenCanvas !== 'undefined' && arg instanceof OffscreenCanvas)
|
||||||
|
|| (typeof ImageData !== 'undefined' && arg instanceof ImageData)
|
||||||
|
}) : []
|
||||||
worker.postMessage({
|
worker.postMessage({
|
||||||
type: prop,
|
type: prop,
|
||||||
|
msgId,
|
||||||
args,
|
args,
|
||||||
}, transfer as any[])
|
}, transfer)
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line unicorn/no-thenable
|
||||||
|
then (onfulfilled: (value: any) => void) {
|
||||||
|
const handler = ({ data }: MessageEvent): void => {
|
||||||
|
if (data.type === 'result' && data.msgId === msgId) {
|
||||||
|
onfulfilled(data.args[0])
|
||||||
|
worker.removeEventListener('message', handler as EventListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
worker.addEventListener('message', handler as EventListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,8 @@ import { Vec3 } from 'vec3'
|
||||||
import { BotEvents } from 'mineflayer'
|
import { BotEvents } from 'mineflayer'
|
||||||
import { proxy } from 'valtio'
|
import { proxy } from 'valtio'
|
||||||
import TypedEmitter from 'typed-emitter'
|
import TypedEmitter from 'typed-emitter'
|
||||||
import { getItemFromBlock } from '../../../src/chatUtils'
|
import { Biome } from 'minecraft-data'
|
||||||
import { delayedIterator } from '../../playground/shared'
|
import { delayedIterator } from '../../playground/shared'
|
||||||
import { playerState } from '../../../src/mineflayer/playerState'
|
|
||||||
import { chunkPos } from './simpleUtils'
|
import { chunkPos } from './simpleUtils'
|
||||||
|
|
||||||
export type ChunkPosKey = string // like '16,16'
|
export type ChunkPosKey = string // like '16,16'
|
||||||
|
|
@ -20,24 +19,34 @@ export type WorldDataEmitterEvents = {
|
||||||
blockUpdate: (data: { pos: Vec3, stateId: number }) => void
|
blockUpdate: (data: { pos: Vec3, stateId: number }) => void
|
||||||
entity: (data: any) => void
|
entity: (data: any) => void
|
||||||
entityMoved: (data: any) => void
|
entityMoved: (data: any) => void
|
||||||
|
playerEntity: (data: any) => void
|
||||||
time: (data: number) => void
|
time: (data: number) => void
|
||||||
renderDistance: (viewDistance: number) => void
|
renderDistance: (viewDistance: number) => void
|
||||||
blockEntities: (data: Record<string, any> | { blockEntities: Record<string, any> }) => void
|
blockEntities: (data: Record<string, any> | { blockEntities: Record<string, any> }) => void
|
||||||
listening: () => void
|
|
||||||
markAsLoaded: (data: { x: number, z: number }) => void
|
markAsLoaded: (data: { x: number, z: number }) => void
|
||||||
unloadChunk: (data: { x: number, z: number }) => void
|
unloadChunk: (data: { x: number, z: number }) => void
|
||||||
loadChunk: (data: { x: number, z: number, chunk: string, blockEntities: any, worldConfig: any, isLightUpdate: boolean }) => void
|
loadChunk: (data: { x: number, z: number, chunk: string, blockEntities: any, worldConfig: any, isLightUpdate: boolean }) => void
|
||||||
updateLight: (data: { pos: Vec3 }) => void
|
updateLight: (data: { pos: Vec3 }) => void
|
||||||
onWorldSwitch: () => void
|
onWorldSwitch: () => void
|
||||||
end: () => void
|
end: () => void
|
||||||
|
biomeUpdate: (data: { biome: Biome }) => void
|
||||||
|
biomeReset: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WorldDataEmitterWorker extends (EventEmitter as new () => TypedEmitter<WorldDataEmitterEvents>) {
|
||||||
|
static readonly restorerName = 'WorldDataEmitterWorker'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Usually connects to mineflayer bot and emits world data (chunks, entities)
|
|
||||||
* It's up to the consumer to serialize the data if needed
|
|
||||||
*/
|
|
||||||
export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<WorldDataEmitterEvents>) {
|
export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<WorldDataEmitterEvents>) {
|
||||||
|
spiralNumber = 0
|
||||||
|
gotPanicLastTime = false
|
||||||
|
panicChunksReload = () => {}
|
||||||
loadedChunks: Record<ChunkPosKey, boolean>
|
loadedChunks: Record<ChunkPosKey, boolean>
|
||||||
|
private inLoading = false
|
||||||
|
private chunkReceiveTimes: number[] = []
|
||||||
|
private lastChunkReceiveTime = 0
|
||||||
|
public lastChunkReceiveTimeAvg = 0
|
||||||
|
private panicTimeout?: NodeJS.Timeout
|
||||||
readonly lastPos: Vec3
|
readonly lastPos: Vec3
|
||||||
private eventListeners: Record<string, any> = {}
|
private eventListeners: Record<string, any> = {}
|
||||||
private readonly emitter: WorldDataEmitter
|
private readonly emitter: WorldDataEmitter
|
||||||
|
|
@ -57,11 +66,6 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
|
||||||
/* config */ isPlayground = false
|
/* config */ isPlayground = false
|
||||||
/* config */ allowPositionUpdate = true
|
/* config */ allowPositionUpdate = true
|
||||||
|
|
||||||
public reactive = proxy({
|
|
||||||
cursorBlock: null as Vec3 | null,
|
|
||||||
cursorBlockBreakingStage: null as number | null,
|
|
||||||
})
|
|
||||||
|
|
||||||
constructor (public world: typeof __type_bot['world'], public viewDistance: number, position: Vec3 = new Vec3(0, 0, 0)) {
|
constructor (public world: typeof __type_bot['world'], public viewDistance: number, position: Vec3 = new Vec3(0, 0, 0)) {
|
||||||
// eslint-disable-next-line constructor-super
|
// eslint-disable-next-line constructor-super
|
||||||
super()
|
super()
|
||||||
|
|
@ -98,13 +102,20 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
|
||||||
})
|
})
|
||||||
|
|
||||||
const emitEntity = (e, name = 'entity') => {
|
const emitEntity = (e, name = 'entity') => {
|
||||||
if (!e || e === bot.entity) return
|
if (!e) return
|
||||||
|
if (e === bot.entity) {
|
||||||
|
if (name === 'entity') {
|
||||||
|
this.emitter.emit('playerEntity', e)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!e.name) return // mineflayer received update for not spawned entity
|
if (!e.name) return // mineflayer received update for not spawned entity
|
||||||
e.objectData = entitiesObjectData.get(e.id)
|
e.objectData = entitiesObjectData.get(e.id)
|
||||||
this.emitter.emit(name as any, {
|
this.emitter.emit(name as any, {
|
||||||
...e,
|
...e,
|
||||||
pos: e.position,
|
pos: e.position,
|
||||||
username: e.username,
|
username: e.username,
|
||||||
|
team: bot.teamMap[e.username] || bot.teamMap[e.uuid],
|
||||||
// set debugTree (obj) {
|
// set debugTree (obj) {
|
||||||
// e.debugTree = obj
|
// e.debugTree = obj
|
||||||
// }
|
// }
|
||||||
|
|
@ -133,12 +144,19 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
|
||||||
this.emitter.emit('entity', { id: e.id, delete: true })
|
this.emitter.emit('entity', { id: e.id, delete: true })
|
||||||
},
|
},
|
||||||
chunkColumnLoad: (pos: Vec3) => {
|
chunkColumnLoad: (pos: Vec3) => {
|
||||||
|
const now = performance.now()
|
||||||
|
if (this.lastChunkReceiveTime) {
|
||||||
|
this.chunkReceiveTimes.push(now - this.lastChunkReceiveTime)
|
||||||
|
}
|
||||||
|
this.lastChunkReceiveTime = now
|
||||||
|
|
||||||
if (this.waitingSpiralChunksLoad[`${pos.x},${pos.z}`]) {
|
if (this.waitingSpiralChunksLoad[`${pos.x},${pos.z}`]) {
|
||||||
this.waitingSpiralChunksLoad[`${pos.x},${pos.z}`](true)
|
this.waitingSpiralChunksLoad[`${pos.x},${pos.z}`](true)
|
||||||
delete this.waitingSpiralChunksLoad[`${pos.x},${pos.z}`]
|
delete this.waitingSpiralChunksLoad[`${pos.x},${pos.z}`]
|
||||||
} else if (this.loadedChunks[`${pos.x},${pos.z}`]) {
|
} else if (this.loadedChunks[`${pos.x},${pos.z}`]) {
|
||||||
void this.loadChunk(pos, false, 'Received another chunkColumnLoad event while already loaded')
|
void this.loadChunk(pos, false, 'Received another chunkColumnLoad event while already loaded')
|
||||||
}
|
}
|
||||||
|
this.chunkProgress()
|
||||||
},
|
},
|
||||||
chunkColumnUnload: (pos: Vec3) => {
|
chunkColumnUnload: (pos: Vec3) => {
|
||||||
this.unloadChunk(pos)
|
this.unloadChunk(pos)
|
||||||
|
|
@ -156,9 +174,11 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
|
||||||
// when dimension might change
|
// when dimension might change
|
||||||
login: () => {
|
login: () => {
|
||||||
void this.updatePosition(bot.entity.position, true)
|
void this.updatePosition(bot.entity.position, true)
|
||||||
|
this.emitter.emit('playerEntity', bot.entity)
|
||||||
},
|
},
|
||||||
respawn: () => {
|
respawn: () => {
|
||||||
void this.updatePosition(bot.entity.position, true)
|
void this.updatePosition(bot.entity.position, true)
|
||||||
|
this.emitter.emit('playerEntity', bot.entity)
|
||||||
this.emitter.emit('onWorldSwitch')
|
this.emitter.emit('onWorldSwitch')
|
||||||
},
|
},
|
||||||
} satisfies Partial<BotEvents>
|
} satisfies Partial<BotEvents>
|
||||||
|
|
@ -171,22 +191,6 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.emitter.on('listening', () => {
|
|
||||||
this.emitter.emit('blockEntities', new Proxy({}, {
|
|
||||||
get (_target, posKey, receiver) {
|
|
||||||
if (typeof posKey !== 'string') return
|
|
||||||
const [x, y, z] = posKey.split(',').map(Number)
|
|
||||||
return bot.world.getBlock(new Vec3(x, y, z))?.entity
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
this.emitter.emit('renderDistance', this.viewDistance)
|
|
||||||
this.emitter.emit('time', bot.time.timeOfDay)
|
|
||||||
})
|
|
||||||
// node.js stream data event pattern
|
|
||||||
if (this.emitter.listenerCount('blockEntities')) {
|
|
||||||
this.emitter.emit('listening')
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [evt, listener] of Object.entries(this.eventListeners)) {
|
for (const [evt, listener] of Object.entries(this.eventListeners)) {
|
||||||
bot.on(evt as any, listener)
|
bot.on(evt as any, listener)
|
||||||
}
|
}
|
||||||
|
|
@ -200,8 +204,16 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
|
||||||
console.error('error processing entity', err)
|
console.error('error processing entity', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void this.init(bot.entity.position)
|
emitterGotConnected () {
|
||||||
|
this.emitter.emit('blockEntities', new Proxy({}, {
|
||||||
|
get (_target, posKey, receiver) {
|
||||||
|
if (typeof posKey !== 'string') return
|
||||||
|
const [x, y, z] = posKey.split(',').map(Number)
|
||||||
|
return bot.world.getBlock(new Vec3(x, y, z))?.entity
|
||||||
|
},
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
removeListenersFromBot (bot: import('mineflayer').Bot) {
|
removeListenersFromBot (bot: import('mineflayer').Bot) {
|
||||||
|
|
@ -213,38 +225,71 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
|
||||||
async init (pos: Vec3) {
|
async init (pos: Vec3) {
|
||||||
this.updateViewDistance(this.viewDistance)
|
this.updateViewDistance(this.viewDistance)
|
||||||
this.emitter.emit('chunkPosUpdate', { pos })
|
this.emitter.emit('chunkPosUpdate', { pos })
|
||||||
|
if (bot?.time?.timeOfDay) {
|
||||||
|
this.emitter.emit('time', bot.time.timeOfDay)
|
||||||
|
}
|
||||||
|
if (bot?.entity) {
|
||||||
|
this.emitter.emit('playerEntity', bot.entity)
|
||||||
|
}
|
||||||
|
this.emitterGotConnected()
|
||||||
const [botX, botZ] = chunkPos(pos)
|
const [botX, botZ] = chunkPos(pos)
|
||||||
|
|
||||||
const positions = generateSpiralMatrix(this.viewDistance).map(([x, z]) => new Vec3((botX + x) * 16, 0, (botZ + z) * 16))
|
const positions = generateSpiralMatrix(this.viewDistance).map(([x, z]) => new Vec3((botX + x) * 16, 0, (botZ + z) * 16))
|
||||||
|
|
||||||
this.lastPos.update(pos)
|
this.lastPos.update(pos)
|
||||||
await this._loadChunks(positions)
|
await this._loadChunks(positions, pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
async _loadChunks (positions: Vec3[], sliceSize = 5) {
|
chunkProgress () {
|
||||||
|
if (this.panicTimeout) clearTimeout(this.panicTimeout)
|
||||||
|
if (this.chunkReceiveTimes.length >= 5) {
|
||||||
|
const avgReceiveTime = this.chunkReceiveTimes.reduce((a, b) => a + b, 0) / this.chunkReceiveTimes.length
|
||||||
|
this.lastChunkReceiveTimeAvg = avgReceiveTime
|
||||||
|
const timeoutDelay = avgReceiveTime * 2 + 1000 // 2x average + 1 second
|
||||||
|
|
||||||
|
// Clear any existing timeout
|
||||||
|
if (this.panicTimeout) clearTimeout(this.panicTimeout)
|
||||||
|
|
||||||
|
// Set new timeout for panic reload
|
||||||
|
this.panicTimeout = setTimeout(() => {
|
||||||
|
if (!this.gotPanicLastTime && this.inLoading) {
|
||||||
|
console.warn('Chunk loading seems stuck, triggering panic reload')
|
||||||
|
this.gotPanicLastTime = true
|
||||||
|
this.panicChunksReload()
|
||||||
|
}
|
||||||
|
}, timeoutDelay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _loadChunks (positions: Vec3[], centerPos: Vec3) {
|
||||||
|
this.spiralNumber++
|
||||||
|
const { spiralNumber } = this
|
||||||
// stop loading previous chunks
|
// stop loading previous chunks
|
||||||
for (const pos of Object.keys(this.waitingSpiralChunksLoad)) {
|
for (const pos of Object.keys(this.waitingSpiralChunksLoad)) {
|
||||||
this.waitingSpiralChunksLoad[pos](false)
|
this.waitingSpiralChunksLoad[pos](false)
|
||||||
delete this.waitingSpiralChunksLoad[pos]
|
delete this.waitingSpiralChunksLoad[pos]
|
||||||
}
|
}
|
||||||
|
|
||||||
const promises = [] as Array<Promise<void>>
|
|
||||||
let continueLoading = true
|
let continueLoading = true
|
||||||
|
this.inLoading = true
|
||||||
await delayedIterator(positions, this.addWaitTime, async (pos) => {
|
await delayedIterator(positions, this.addWaitTime, async (pos) => {
|
||||||
const promise = (async () => {
|
if (!continueLoading || this.loadedChunks[`${pos.x},${pos.z}`]) return
|
||||||
if (!continueLoading || this.loadedChunks[`${pos.x},${pos.z}`]) return
|
|
||||||
|
|
||||||
if (!this.world.getColumnAt(pos)) {
|
// Wait for chunk to be available from server
|
||||||
continueLoading = await new Promise<boolean>(resolve => {
|
if (!this.world.getColumnAt(pos)) {
|
||||||
this.waitingSpiralChunksLoad[`${pos.x},${pos.z}`] = resolve
|
continueLoading = await new Promise<boolean>(resolve => {
|
||||||
})
|
this.waitingSpiralChunksLoad[`${pos.x},${pos.z}`] = resolve
|
||||||
}
|
})
|
||||||
if (!continueLoading) return
|
}
|
||||||
await this.loadChunk(pos)
|
if (!continueLoading) return
|
||||||
})()
|
await this.loadChunk(pos, undefined, `spiral ${spiralNumber} from ${centerPos.x},${centerPos.z}`)
|
||||||
promises.push(promise)
|
this.chunkProgress()
|
||||||
})
|
})
|
||||||
await Promise.all(promises)
|
if (this.panicTimeout) clearTimeout(this.panicTimeout)
|
||||||
|
this.inLoading = false
|
||||||
|
this.gotPanicLastTime = false
|
||||||
|
this.chunkReceiveTimes = []
|
||||||
|
this.lastChunkReceiveTime = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
readdDebug () {
|
readdDebug () {
|
||||||
|
|
@ -318,8 +363,37 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
|
||||||
delete this.debugChunksInfo[`${pos.x},${pos.z}`]
|
delete this.debugChunksInfo[`${pos.x},${pos.z}`]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lastBiomeId: number | null = null
|
||||||
|
|
||||||
|
udpateBiome (pos: Vec3) {
|
||||||
|
try {
|
||||||
|
const biomeId = this.world.getBiome(pos)
|
||||||
|
if (biomeId !== this.lastBiomeId) {
|
||||||
|
this.lastBiomeId = biomeId
|
||||||
|
const biomeData = loadedData.biomes[biomeId]
|
||||||
|
if (biomeData) {
|
||||||
|
this.emitter.emit('biomeUpdate', {
|
||||||
|
biome: biomeData
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// unknown biome
|
||||||
|
this.emitter.emit('biomeReset')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('error updating biome', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastPosCheck: Vec3 | null = null
|
||||||
async updatePosition (pos: Vec3, force = false) {
|
async updatePosition (pos: Vec3, force = false) {
|
||||||
if (!this.allowPositionUpdate) return
|
if (!this.allowPositionUpdate) return
|
||||||
|
const posFloored = pos.floored()
|
||||||
|
if (!force && this.lastPosCheck && this.lastPosCheck.equals(posFloored)) return
|
||||||
|
this.lastPosCheck = posFloored
|
||||||
|
|
||||||
|
this.udpateBiome(pos)
|
||||||
|
|
||||||
const [lastX, lastZ] = chunkPos(this.lastPos)
|
const [lastX, lastZ] = chunkPos(this.lastPos)
|
||||||
const [botX, botZ] = chunkPos(pos)
|
const [botX, botZ] = chunkPos(pos)
|
||||||
if (lastX !== botX || lastZ !== botZ || force) {
|
if (lastX !== botX || lastZ !== botZ || force) {
|
||||||
|
|
@ -337,7 +411,6 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
|
||||||
chunksToUnload.push(p)
|
chunksToUnload.push(p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('unloading', chunksToUnload.length, 'total now', Object.keys(this.loadedChunks).length)
|
|
||||||
for (const p of chunksToUnload) {
|
for (const p of chunksToUnload) {
|
||||||
this.unloadChunk(p)
|
this.unloadChunk(p)
|
||||||
}
|
}
|
||||||
|
|
@ -349,7 +422,7 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
|
||||||
return undefined!
|
return undefined!
|
||||||
}).filter(a => !!a)
|
}).filter(a => !!a)
|
||||||
this.lastPos.update(pos)
|
this.lastPos.update(pos)
|
||||||
void this._loadChunks(positions)
|
void this._loadChunks(positions, pos)
|
||||||
} else {
|
} else {
|
||||||
this.emitter.emit('chunkPosUpdate', { pos }) // todo-low
|
this.emitter.emit('chunkPosUpdate', { pos }) // todo-low
|
||||||
this.lastPos.update(pos)
|
this.lastPos.update(pos)
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,22 @@
|
||||||
/* eslint-disable guard-for-in */
|
/* eslint-disable guard-for-in */
|
||||||
import { EventEmitter } from 'events'
|
import { EventEmitter } from 'events'
|
||||||
import { Vec3 } from 'vec3'
|
import { Vec3 } from 'vec3'
|
||||||
import * as THREE from 'three'
|
|
||||||
import mcDataRaw from 'minecraft-data/data.js' // note: using alias
|
import mcDataRaw from 'minecraft-data/data.js' // note: using alias
|
||||||
import TypedEmitter from 'typed-emitter'
|
import TypedEmitter from 'typed-emitter'
|
||||||
import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer'
|
|
||||||
import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
|
import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
|
||||||
import { generateSpiralMatrix } from 'flying-squid/dist/utils'
|
import { generateSpiralMatrix } from 'flying-squid/dist/utils'
|
||||||
import { subscribeKey } from 'valtio/utils'
|
import { subscribeKey } from 'valtio/utils'
|
||||||
import { proxy } from 'valtio'
|
import { proxy } from 'valtio'
|
||||||
import { dynamicMcDataFiles } from '../../buildMesherConfig.mjs'
|
import { dynamicMcDataFiles } from '../../buildMesherConfig.mjs'
|
||||||
import { toMajorVersion } from '../../../src/utils'
|
import type { ResourcesManagerTransferred } from '../../../src/resourcesManager'
|
||||||
import { ResourcesManager } from '../../../src/resourcesManager'
|
|
||||||
import { DisplayWorldOptions, GraphicsInitOptions, RendererReactiveState } from '../../../src/appViewer'
|
import { DisplayWorldOptions, GraphicsInitOptions, RendererReactiveState } from '../../../src/appViewer'
|
||||||
import { SoundSystem } from '../three/threeJsSound'
|
import { SoundSystem } from '../three/threeJsSound'
|
||||||
import { buildCleanupDecorator } from './cleanupDecorator'
|
import { buildCleanupDecorator } from './cleanupDecorator'
|
||||||
import { HighestBlockInfo, MesherGeometryOutput, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig, MesherMainEvent } from './mesher/shared'
|
import { HighestBlockInfo, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig, MesherMainEvent } from './mesher/shared'
|
||||||
import { chunkPos } from './simpleUtils'
|
import { chunkPos } from './simpleUtils'
|
||||||
import { addNewStat, removeAllStats, removeStat, updatePanesVisibility, updateStatText } from './ui/newStats'
|
import { addNewStat, removeAllStats, updatePanesVisibility, updateStatText } from './ui/newStats'
|
||||||
import { WorldDataEmitter } from './worldDataEmitter'
|
import { WorldDataEmitterWorker } from './worldDataEmitter'
|
||||||
import { IPlayerState } from './basePlayerState'
|
import { getPlayerStateUtils, PlayerStateReactive, PlayerStateRenderer, PlayerStateUtils } from './basePlayerState'
|
||||||
import { MesherLogReader } from './mesherlogReader'
|
import { MesherLogReader } from './mesherlogReader'
|
||||||
import { setSkinsConfig } from './utils/skins'
|
import { setSkinsConfig } from './utils/skins'
|
||||||
|
|
||||||
|
|
@ -27,32 +24,53 @@ function mod (x, n) {
|
||||||
return ((x % n) + n) % n
|
return ((x % n) + n) % n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toMajorVersion = version => {
|
||||||
|
const [a, b] = (String(version)).split('.')
|
||||||
|
return `${a}.${b}`
|
||||||
|
}
|
||||||
|
|
||||||
export const worldCleanup = buildCleanupDecorator('resetWorld')
|
export const worldCleanup = buildCleanupDecorator('resetWorld')
|
||||||
|
|
||||||
export const defaultWorldRendererConfig = {
|
export const defaultWorldRendererConfig = {
|
||||||
|
// Debug settings
|
||||||
showChunkBorders: false,
|
showChunkBorders: false,
|
||||||
|
enableDebugOverlay: false,
|
||||||
|
|
||||||
|
// Performance settings
|
||||||
mesherWorkers: 4,
|
mesherWorkers: 4,
|
||||||
isPlayground: false,
|
addChunksBatchWaitTime: 200,
|
||||||
renderEars: true,
|
_experimentalSmoothChunkLoading: true,
|
||||||
// game renderer setting actually
|
_renderByChunks: false,
|
||||||
showHand: false,
|
|
||||||
viewBobbing: false,
|
// Rendering engine settings
|
||||||
extraBlockRenderers: true,
|
dayCycle: true,
|
||||||
clipWorldBelowY: undefined as number | undefined,
|
|
||||||
smoothLighting: true,
|
smoothLighting: true,
|
||||||
enableLighting: true,
|
enableLighting: true,
|
||||||
starfield: true,
|
starfield: true,
|
||||||
addChunksBatchWaitTime: 200,
|
defaultSkybox: true,
|
||||||
|
renderEntities: true,
|
||||||
|
extraBlockRenderers: true,
|
||||||
|
foreground: true,
|
||||||
|
fov: 75,
|
||||||
|
volume: 1,
|
||||||
|
|
||||||
|
// Camera visual related settings
|
||||||
|
showHand: false,
|
||||||
|
viewBobbing: false,
|
||||||
|
renderEars: true,
|
||||||
|
highlightBlockColor: 'blue',
|
||||||
|
|
||||||
|
// Player models
|
||||||
|
fetchPlayerSkins: true,
|
||||||
|
skinTexturesProxy: undefined as string | undefined,
|
||||||
|
|
||||||
|
// VR settings
|
||||||
vrSupport: true,
|
vrSupport: true,
|
||||||
vrPageGameRendering: true,
|
vrPageGameRendering: true,
|
||||||
renderEntities: true,
|
|
||||||
fov: 75,
|
// World settings
|
||||||
fetchPlayerSkins: true,
|
clipWorldBelowY: undefined as number | undefined,
|
||||||
highlightBlockColor: 'blue',
|
isPlayground: false
|
||||||
foreground: true,
|
|
||||||
enableDebugOverlay: false,
|
|
||||||
_experimentalSmoothChunkLoading: true,
|
|
||||||
_renderByChunks: false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorldRendererConfig = typeof defaultWorldRendererConfig
|
export type WorldRendererConfig = typeof defaultWorldRendererConfig
|
||||||
|
|
@ -103,7 +121,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
}>
|
}>
|
||||||
customTexturesDataUrl = undefined as string | undefined
|
customTexturesDataUrl = undefined as string | undefined
|
||||||
workers: any[] = []
|
workers: any[] = []
|
||||||
viewerPosition?: Vec3
|
viewerChunkPosition?: Vec3
|
||||||
lastCamUpdate = 0
|
lastCamUpdate = 0
|
||||||
droppedFpsPercentage = 0
|
droppedFpsPercentage = 0
|
||||||
initialChunkLoadWasStartedIn: number | undefined
|
initialChunkLoadWasStartedIn: number | undefined
|
||||||
|
|
@ -119,7 +137,6 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
|
|
||||||
handleResize = () => { }
|
handleResize = () => { }
|
||||||
highestBlocksByChunks = new Map<string, { [chunkKey: string]: HighestBlockInfo }>()
|
highestBlocksByChunks = new Map<string, { [chunkKey: string]: HighestBlockInfo }>()
|
||||||
highestBlocksBySections = new Map<string, { [sectionKey: string]: HighestBlockInfo }>()
|
|
||||||
blockEntities = {}
|
blockEntities = {}
|
||||||
|
|
||||||
workersProcessAverageTime = 0
|
workersProcessAverageTime = 0
|
||||||
|
|
@ -153,7 +170,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
abstract changeBackgroundColor (color: [number, number, number]): void
|
abstract changeBackgroundColor (color: [number, number, number]): void
|
||||||
|
|
||||||
worldRendererConfig: WorldRendererConfig
|
worldRendererConfig: WorldRendererConfig
|
||||||
playerState: IPlayerState
|
playerStateReactive: PlayerStateReactive
|
||||||
|
playerStateUtils: PlayerStateUtils
|
||||||
reactiveState: RendererReactiveState
|
reactiveState: RendererReactiveState
|
||||||
mesherLogReader: MesherLogReader | undefined
|
mesherLogReader: MesherLogReader | undefined
|
||||||
forceCallFromMesherReplayer = false
|
forceCallFromMesherReplayer = false
|
||||||
|
|
@ -169,6 +187,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
}
|
}
|
||||||
currentRenderedFrames = 0
|
currentRenderedFrames = 0
|
||||||
fpsAverage = 0
|
fpsAverage = 0
|
||||||
|
lastFps = 0
|
||||||
fpsWorst = undefined as number | undefined
|
fpsWorst = undefined as number | undefined
|
||||||
fpsSamples = 0
|
fpsSamples = 0
|
||||||
mainThreadRendering = true
|
mainThreadRendering = true
|
||||||
|
|
@ -184,10 +203,11 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
return (this.initOptions.config.statsVisible ?? 0) > 1
|
return (this.initOptions.config.statsVisible ?? 0) > 1
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor (public readonly resourcesManager: ResourcesManager, public displayOptions: DisplayWorldOptions, public initOptions: GraphicsInitOptions) {
|
constructor (public readonly resourcesManager: ResourcesManagerTransferred, public displayOptions: DisplayWorldOptions, public initOptions: GraphicsInitOptions) {
|
||||||
this.snapshotInitialValues()
|
this.snapshotInitialValues()
|
||||||
this.worldRendererConfig = displayOptions.inWorldRenderingConfig
|
this.worldRendererConfig = displayOptions.inWorldRenderingConfig
|
||||||
this.playerState = displayOptions.playerState
|
this.playerStateReactive = displayOptions.playerStateReactive
|
||||||
|
this.playerStateUtils = getPlayerStateUtils(this.playerStateReactive)
|
||||||
this.reactiveState = displayOptions.rendererState
|
this.reactiveState = displayOptions.rendererState
|
||||||
// this.mesherLogReader = new MesherLogReader(this)
|
// this.mesherLogReader = new MesherLogReader(this)
|
||||||
this.renderUpdateEmitter.on('update', () => {
|
this.renderUpdateEmitter.on('update', () => {
|
||||||
|
|
@ -221,6 +241,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
} else {
|
} else {
|
||||||
this.fpsWorst = Math.min(this.fpsWorst, this.currentRenderedFrames)
|
this.fpsWorst = Math.min(this.fpsWorst, this.currentRenderedFrames)
|
||||||
}
|
}
|
||||||
|
this.lastFps = this.currentRenderedFrames
|
||||||
this.currentRenderedFrames = 0
|
this.currentRenderedFrames = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -231,15 +252,11 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
|
|
||||||
async init () {
|
async init () {
|
||||||
if (this.active) throw new Error('WorldRendererCommon is already initialized')
|
if (this.active) throw new Error('WorldRendererCommon is already initialized')
|
||||||
await this.resourcesManager.loadMcData(this.version)
|
|
||||||
if (!this.resourcesManager.currentResources) {
|
|
||||||
await this.resourcesManager.updateAssetsData({ })
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.resetWorkers(),
|
this.resetWorkers(),
|
||||||
(async () => {
|
(async () => {
|
||||||
if (this.resourcesManager.currentResources) {
|
if (this.resourcesManager.currentResources?.allReady) {
|
||||||
await this.updateAssetsData()
|
await this.updateAssetsData()
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
@ -291,36 +308,23 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
initWorkers (numWorkers = this.worldRendererConfig.mesherWorkers) {
|
initWorkers (numWorkers = this.worldRendererConfig.mesherWorkers) {
|
||||||
// init workers
|
// init workers
|
||||||
for (let i = 0; i < numWorkers + 1; i++) {
|
for (let i = 0; i < numWorkers + 1; i++) {
|
||||||
// Node environment needs an absolute path, but browser needs the url of the file
|
const worker = initMesherWorker((data) => {
|
||||||
const workerName = 'mesher.js'
|
|
||||||
// eslint-disable-next-line node/no-path-concat
|
|
||||||
const src = typeof window === 'undefined' ? `${__dirname}/${workerName}` : workerName
|
|
||||||
|
|
||||||
let worker: any
|
|
||||||
if (process.env.SINGLE_FILE_BUILD) {
|
|
||||||
const workerCode = document.getElementById('mesher-worker-code')!.textContent!
|
|
||||||
const blob = new Blob([workerCode], { type: 'text/javascript' })
|
|
||||||
worker = new Worker(window.URL.createObjectURL(blob))
|
|
||||||
} else {
|
|
||||||
worker = new Worker(src)
|
|
||||||
}
|
|
||||||
|
|
||||||
worker.onmessage = ({ data }) => {
|
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
this.messageQueue.push(...data)
|
this.messageQueue.push(...data)
|
||||||
} else {
|
} else {
|
||||||
this.messageQueue.push(data)
|
this.messageQueue.push(data)
|
||||||
}
|
}
|
||||||
void this.processMessageQueue('worker')
|
void this.processMessageQueue('worker')
|
||||||
}
|
})
|
||||||
if (worker.on) worker.on('message', (data) => { worker.onmessage({ data }) })
|
|
||||||
this.workers.push(worker)
|
this.workers.push(worker)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onReactiveValueUpdated<T extends keyof typeof this.displayOptions.playerState.reactive>(key: T, callback: (value: typeof this.displayOptions.playerState.reactive[T]) => void) {
|
onReactivePlayerStateUpdated<T extends keyof PlayerStateReactive>(key: T, callback: (value: PlayerStateReactive[T]) => void, initial = true) {
|
||||||
callback(this.displayOptions.playerState.reactive[key])
|
if (initial) {
|
||||||
subscribeKey(this.displayOptions.playerState.reactive, key, callback)
|
callback(this.playerStateReactive[key])
|
||||||
|
}
|
||||||
|
subscribeKey(this.playerStateReactive, key, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
onReactiveConfigUpdated<T extends keyof typeof this.worldRendererConfig>(key: T, callback: (value: typeof this.worldRendererConfig[T]) => void) {
|
onReactiveConfigUpdated<T extends keyof typeof this.worldRendererConfig>(key: T, callback: (value: typeof this.worldRendererConfig[T]) => void) {
|
||||||
|
|
@ -334,7 +338,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
}
|
}
|
||||||
|
|
||||||
watchReactivePlayerState () {
|
watchReactivePlayerState () {
|
||||||
this.onReactiveValueUpdated('backgroundColor', (value) => {
|
this.onReactivePlayerStateUpdated('backgroundColor', (value) => {
|
||||||
this.changeBackgroundColor(value)
|
this.changeBackgroundColor(value)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -398,8 +402,6 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
this.logWorkerWork(() => `-> ${data.workerIndex} geometry ${data.key} ${JSON.stringify({ dataSize: JSON.stringify(data).length })}`)
|
this.logWorkerWork(() => `-> ${data.workerIndex} geometry ${data.key} ${JSON.stringify({ dataSize: JSON.stringify(data).length })}`)
|
||||||
this.geometryReceiveCount[data.workerIndex] ??= 0
|
this.geometryReceiveCount[data.workerIndex] ??= 0
|
||||||
this.geometryReceiveCount[data.workerIndex]++
|
this.geometryReceiveCount[data.workerIndex]++
|
||||||
const { geometry } = data
|
|
||||||
this.highestBlocksBySections[data.key] = geometry.highestBlocks
|
|
||||||
const chunkCoords = data.key.split(',').map(Number)
|
const chunkCoords = data.key.split(',').map(Number)
|
||||||
this.lastChunkDistance = Math.max(...this.getDistance(new Vec3(chunkCoords[0], 0, chunkCoords[2])))
|
this.lastChunkDistance = Math.max(...this.getDistance(new Vec3(chunkCoords[0], 0, chunkCoords[2])))
|
||||||
}
|
}
|
||||||
|
|
@ -466,7 +468,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.type === 'heightmap') {
|
if (data.type === 'heightmap') {
|
||||||
appViewer.rendererState.world.heightmaps.set(data.key, new Uint8Array(data.heightmap))
|
this.reactiveState.world.heightmaps.set(data.key, new Uint8Array(data.heightmap))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -508,8 +510,12 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
|
|
||||||
timeUpdated? (newTime: number): void
|
timeUpdated? (newTime: number): void
|
||||||
|
|
||||||
|
biomeUpdated? (biome: any): void
|
||||||
|
|
||||||
|
biomeReset? (): void
|
||||||
|
|
||||||
updateViewerPosition (pos: Vec3) {
|
updateViewerPosition (pos: Vec3) {
|
||||||
this.viewerPosition = pos
|
this.viewerChunkPosition = pos
|
||||||
for (const [key, value] of Object.entries(this.loadedChunks)) {
|
for (const [key, value] of Object.entries(this.loadedChunks)) {
|
||||||
if (!value) continue
|
if (!value) continue
|
||||||
this.updatePosDataChunk?.(key)
|
this.updatePosDataChunk?.(key)
|
||||||
|
|
@ -523,7 +529,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
}
|
}
|
||||||
|
|
||||||
getDistance (posAbsolute: Vec3) {
|
getDistance (posAbsolute: Vec3) {
|
||||||
const [botX, botZ] = chunkPos(this.viewerPosition!)
|
const [botX, botZ] = chunkPos(this.viewerChunkPosition!)
|
||||||
const dx = Math.abs(botX - Math.floor(posAbsolute.x / 16))
|
const dx = Math.abs(botX - Math.floor(posAbsolute.x / 16))
|
||||||
const dz = Math.abs(botZ - Math.floor(posAbsolute.z / 16))
|
const dz = Math.abs(botZ - Math.floor(posAbsolute.z / 16))
|
||||||
return [dx, dz] as [number, number]
|
return [dx, dz] as [number, number]
|
||||||
|
|
@ -543,7 +549,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
this.resetWorld()
|
this.resetWorld()
|
||||||
|
|
||||||
// for workers in single file build
|
// for workers in single file build
|
||||||
if (document?.readyState === 'loading') {
|
if (typeof document !== 'undefined' && document?.readyState === 'loading') {
|
||||||
await new Promise(resolve => {
|
await new Promise(resolve => {
|
||||||
document.addEventListener('DOMContentLoaded', resolve)
|
document.addEventListener('DOMContentLoaded', resolve)
|
||||||
})
|
})
|
||||||
|
|
@ -575,7 +581,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
skyLight,
|
skyLight,
|
||||||
smoothLighting: this.worldRendererConfig.smoothLighting,
|
smoothLighting: this.worldRendererConfig.smoothLighting,
|
||||||
outputFormat: this.outputFormat,
|
outputFormat: this.outputFormat,
|
||||||
textureSize: this.resourcesManager.currentResources!.blocksAtlasParser.atlas.latest.width,
|
// textureSize: this.resourcesManager.currentResources!.blocksAtlasParser.atlas.latest.width,
|
||||||
debugModelVariant: undefined,
|
debugModelVariant: undefined,
|
||||||
clipWorldBelowY: this.worldRendererConfig.clipWorldBelowY,
|
clipWorldBelowY: this.worldRendererConfig.clipWorldBelowY,
|
||||||
disableSignsMapsSupport: !this.worldRendererConfig.extraBlockRenderers,
|
disableSignsMapsSupport: !this.worldRendererConfig.extraBlockRenderers,
|
||||||
|
|
@ -600,7 +606,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateAssetsData () {
|
async updateAssetsData () {
|
||||||
const resources = this.resourcesManager.currentResources!
|
const resources = this.resourcesManager.currentResources
|
||||||
|
|
||||||
if (this.workers.length === 0) throw new Error('workers not initialized yet')
|
if (this.workers.length === 0) throw new Error('workers not initialized yet')
|
||||||
for (const [i, worker] of this.workers.entries()) {
|
for (const [i, worker] of this.workers.entries()) {
|
||||||
|
|
@ -610,7 +616,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
type: 'mesherData',
|
type: 'mesherData',
|
||||||
workerIndex: i,
|
workerIndex: i,
|
||||||
blocksAtlas: {
|
blocksAtlas: {
|
||||||
latest: resources.blocksAtlasParser.atlas.latest
|
latest: resources.blocksAtlasJson
|
||||||
},
|
},
|
||||||
blockstatesModels,
|
blockstatesModels,
|
||||||
config: this.getMesherConfig(),
|
config: this.getMesherConfig(),
|
||||||
|
|
@ -697,7 +703,6 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
for (let y = this.worldSizeParams.minY; y < this.worldSizeParams.worldHeight; y += 16) {
|
for (let y = this.worldSizeParams.minY; y < this.worldSizeParams.worldHeight; y += 16) {
|
||||||
this.setSectionDirty(new Vec3(x, y, z), false)
|
this.setSectionDirty(new Vec3(x, y, z), false)
|
||||||
delete this.finishedSections[`${x},${y},${z}`]
|
delete this.finishedSections[`${x},${y},${z}`]
|
||||||
this.highestBlocksBySections.delete(`${x},${y},${z}`)
|
|
||||||
}
|
}
|
||||||
this.highestBlocksByChunks.delete(`${x},${z}`)
|
this.highestBlocksByChunks.delete(`${x},${z}`)
|
||||||
|
|
||||||
|
|
@ -731,9 +736,11 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
|
|
||||||
updateEntity (e: any, isUpdate = false) { }
|
updateEntity (e: any, isUpdate = false) { }
|
||||||
|
|
||||||
|
abstract updatePlayerEntity? (e: any): void
|
||||||
|
|
||||||
lightUpdate (chunkX: number, chunkZ: number) { }
|
lightUpdate (chunkX: number, chunkZ: number) { }
|
||||||
|
|
||||||
connect (worldView: WorldDataEmitter) {
|
connect (worldView: WorldDataEmitterWorker) {
|
||||||
const worldEmitter = worldView
|
const worldEmitter = worldView
|
||||||
|
|
||||||
worldEmitter.on('entity', (e) => {
|
worldEmitter.on('entity', (e) => {
|
||||||
|
|
@ -742,6 +749,9 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
worldEmitter.on('entityMoved', (e) => {
|
worldEmitter.on('entityMoved', (e) => {
|
||||||
this.updateEntity(e, true)
|
this.updateEntity(e, true)
|
||||||
})
|
})
|
||||||
|
worldEmitter.on('playerEntity', (e) => {
|
||||||
|
this.updatePlayerEntity?.(e)
|
||||||
|
})
|
||||||
|
|
||||||
let currentLoadChunkBatch = null as {
|
let currentLoadChunkBatch = null as {
|
||||||
timeout
|
timeout
|
||||||
|
|
@ -812,16 +822,22 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
})
|
})
|
||||||
|
|
||||||
worldEmitter.on('onWorldSwitch', () => {
|
worldEmitter.on('onWorldSwitch', () => {
|
||||||
for (const fn of this.onWorldSwitched) fn()
|
for (const fn of this.onWorldSwitched) {
|
||||||
|
try {
|
||||||
|
fn()
|
||||||
|
} catch (e) {
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('[Renderer Backend] Error in onWorldSwitched:')
|
||||||
|
throw e
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
worldEmitter.on('time', (timeOfDay) => {
|
worldEmitter.on('time', (timeOfDay) => {
|
||||||
|
if (!this.worldRendererConfig.dayCycle) return
|
||||||
this.timeUpdated?.(timeOfDay)
|
this.timeUpdated?.(timeOfDay)
|
||||||
|
|
||||||
if (timeOfDay < 0 || timeOfDay > 24_000) {
|
|
||||||
throw new Error('Invalid time of day. It should be between 0 and 24000.')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.timeOfTheDay = timeOfDay
|
this.timeOfTheDay = timeOfDay
|
||||||
|
|
||||||
// if (this.worldRendererConfig.skyLight === skyLight) return
|
// if (this.worldRendererConfig.skyLight === skyLight) return
|
||||||
|
|
@ -831,7 +847,13 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
// }
|
// }
|
||||||
})
|
})
|
||||||
|
|
||||||
worldEmitter.emit('listening')
|
worldEmitter.on('biomeUpdate', ({ biome }) => {
|
||||||
|
this.biomeUpdated?.(biome)
|
||||||
|
})
|
||||||
|
|
||||||
|
worldEmitter.on('biomeReset', () => {
|
||||||
|
this.biomeReset?.()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
setBlockStateIdInner (pos: Vec3, stateId: number | undefined, needAoRecalculation = true) {
|
setBlockStateIdInner (pos: Vec3, stateId: number | undefined, needAoRecalculation = true) {
|
||||||
|
|
@ -1029,3 +1051,37 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
removeAllStats()
|
removeAllStats()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const initMesherWorker = (onGotMessage: (data: any) => void) => {
|
||||||
|
// Node environment needs an absolute path, but browser needs the url of the file
|
||||||
|
const workerName = 'mesher.js'
|
||||||
|
|
||||||
|
let worker: any
|
||||||
|
if (process.env.SINGLE_FILE_BUILD) {
|
||||||
|
const workerCode = document.getElementById('mesher-worker-code')!.textContent!
|
||||||
|
const blob = new Blob([workerCode], { type: 'text/javascript' })
|
||||||
|
worker = new Worker(window.URL.createObjectURL(blob))
|
||||||
|
} else {
|
||||||
|
worker = new Worker(workerName)
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.onmessage = ({ data }) => {
|
||||||
|
onGotMessage(data)
|
||||||
|
}
|
||||||
|
if (worker.on) worker.on('message', (data) => { worker.onmessage({ data }) })
|
||||||
|
return worker
|
||||||
|
}
|
||||||
|
|
||||||
|
export const meshersSendMcData = (workers: Worker[], version: string, addData = {} as Record<string, any>) => {
|
||||||
|
const allMcData = mcDataRaw.pc[version] ?? mcDataRaw.pc[toMajorVersion(version)]
|
||||||
|
const mcData = {
|
||||||
|
version: JSON.parse(JSON.stringify(allMcData.version))
|
||||||
|
}
|
||||||
|
for (const key of dynamicMcDataFiles) {
|
||||||
|
mcData[key] = allMcData[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const worker of workers) {
|
||||||
|
worker.postMessage({ type: 'mcData', mcData, ...addData })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { fromFormattedString, render, RenderNode, TextComponent } from '@xmcl/text-component'
|
|
||||||
import type { ChatMessage } from 'prismarine-chat'
|
import type { ChatMessage } from 'prismarine-chat'
|
||||||
|
import { createCanvas } from '../lib/utils'
|
||||||
|
|
||||||
type SignBlockEntity = {
|
type SignBlockEntity = {
|
||||||
Color?: string
|
Color?: string
|
||||||
|
|
@ -32,29 +32,40 @@ const parseSafe = (text: string, task: string) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const renderSign = (blockEntity: SignBlockEntity, PrismarineChat: typeof ChatMessage, ctxHook = (ctx) => { }) => {
|
const LEGACY_COLORS = {
|
||||||
|
black: '#000000',
|
||||||
|
dark_blue: '#0000AA',
|
||||||
|
dark_green: '#00AA00',
|
||||||
|
dark_aqua: '#00AAAA',
|
||||||
|
dark_red: '#AA0000',
|
||||||
|
dark_purple: '#AA00AA',
|
||||||
|
gold: '#FFAA00',
|
||||||
|
gray: '#AAAAAA',
|
||||||
|
dark_gray: '#555555',
|
||||||
|
blue: '#5555FF',
|
||||||
|
green: '#55FF55',
|
||||||
|
aqua: '#55FFFF',
|
||||||
|
red: '#FF5555',
|
||||||
|
light_purple: '#FF55FF',
|
||||||
|
yellow: '#FFFF55',
|
||||||
|
white: '#FFFFFF',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const renderSign = (
|
||||||
|
blockEntity: SignBlockEntity,
|
||||||
|
isHanging: boolean,
|
||||||
|
PrismarineChat: typeof ChatMessage,
|
||||||
|
ctxHook = (ctx) => { },
|
||||||
|
canvasCreator = (width, height): OffscreenCanvas => { return createCanvas(width, height) }
|
||||||
|
) => {
|
||||||
// todo don't use texture rendering, investigate the font rendering when possible
|
// todo don't use texture rendering, investigate the font rendering when possible
|
||||||
// or increase factor when needed
|
// or increase factor when needed
|
||||||
const factor = 40
|
const factor = 40
|
||||||
|
const fontSize = 1.6 * factor
|
||||||
const signboardY = [16, 9]
|
const signboardY = [16, 9]
|
||||||
const heightOffset = signboardY[0] - signboardY[1]
|
const heightOffset = signboardY[0] - signboardY[1]
|
||||||
const heightScalar = heightOffset / 16
|
const heightScalar = heightOffset / 16
|
||||||
|
// todo the text should be clipped based on it's render width (needs investigate)
|
||||||
let canvas: HTMLCanvasElement | undefined
|
|
||||||
let _ctx: CanvasRenderingContext2D | null = null
|
|
||||||
const getCtx = () => {
|
|
||||||
if (_ctx) return _ctx
|
|
||||||
canvas = document.createElement('canvas')
|
|
||||||
|
|
||||||
canvas.width = 16 * factor
|
|
||||||
canvas.height = heightOffset * factor
|
|
||||||
|
|
||||||
_ctx = canvas.getContext('2d')!
|
|
||||||
_ctx.imageSmoothingEnabled = false
|
|
||||||
|
|
||||||
ctxHook(_ctx)
|
|
||||||
return _ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
const texts = 'front_text' in blockEntity ? /* > 1.20 */ blockEntity.front_text.messages : [
|
const texts = 'front_text' in blockEntity ? /* > 1.20 */ blockEntity.front_text.messages : [
|
||||||
blockEntity.Text1,
|
blockEntity.Text1,
|
||||||
|
|
@ -62,78 +73,144 @@ export const renderSign = (blockEntity: SignBlockEntity, PrismarineChat: typeof
|
||||||
blockEntity.Text3,
|
blockEntity.Text3,
|
||||||
blockEntity.Text4
|
blockEntity.Text4
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if (!texts.some((text) => text !== 'null')) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = canvasCreator(16 * factor, heightOffset * factor)
|
||||||
|
|
||||||
|
const _ctx = canvas.getContext('2d')!
|
||||||
|
|
||||||
|
ctxHook(_ctx)
|
||||||
const defaultColor = ('front_text' in blockEntity ? blockEntity.front_text.color : blockEntity.Color) || 'black'
|
const defaultColor = ('front_text' in blockEntity ? blockEntity.front_text.color : blockEntity.Color) || 'black'
|
||||||
for (const [lineNum, text] of texts.slice(0, 4).entries()) {
|
for (const [lineNum, text] of texts.slice(0, 4).entries()) {
|
||||||
// todo: in pre flatenning it seems the format was not json
|
|
||||||
if (text === 'null') continue
|
if (text === 'null') continue
|
||||||
const parsed = text?.startsWith('{') || text?.startsWith('"') ? parseSafe(text ?? '""', 'sign text') : text
|
renderComponent(text, PrismarineChat, canvas, fontSize, defaultColor, fontSize * (lineNum + 1) + (isHanging ? 0 : -8))
|
||||||
if (!parsed || (typeof parsed !== 'object' && typeof parsed !== 'string')) continue
|
|
||||||
// todo fix type
|
|
||||||
const message = typeof parsed === 'string' ? fromFormattedString(parsed) : new PrismarineChat(parsed) as never
|
|
||||||
const patchExtra = ({ extra }: TextComponent) => {
|
|
||||||
if (!extra) return
|
|
||||||
for (const child of extra) {
|
|
||||||
if (child.color) {
|
|
||||||
child.color = child.color === 'dark_green' ? child.color.toUpperCase() : child.color.toLowerCase()
|
|
||||||
}
|
|
||||||
patchExtra(child)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
patchExtra(message)
|
|
||||||
const rendered = render(message)
|
|
||||||
|
|
||||||
const toRenderCanvas: Array<{
|
|
||||||
fontStyle: string
|
|
||||||
fillStyle: string
|
|
||||||
underlineStyle: boolean
|
|
||||||
strikeStyle: boolean
|
|
||||||
text: string
|
|
||||||
}> = []
|
|
||||||
let plainText = ''
|
|
||||||
// todo the text should be clipped based on it's render width (needs investigate)
|
|
||||||
const MAX_LENGTH = 50 // avoid abusing the signboard
|
|
||||||
const renderText = (node: RenderNode) => {
|
|
||||||
const { component } = node
|
|
||||||
let { text } = component
|
|
||||||
if (plainText.length + text.length > MAX_LENGTH) {
|
|
||||||
text = text.slice(0, MAX_LENGTH - plainText.length)
|
|
||||||
if (!text) return false
|
|
||||||
}
|
|
||||||
plainText += text
|
|
||||||
toRenderCanvas.push({
|
|
||||||
fontStyle: `${component.bold ? 'bold' : ''} ${component.italic ? 'italic' : ''}`,
|
|
||||||
fillStyle: node.style['color'] || defaultColor,
|
|
||||||
underlineStyle: component.underlined ?? false,
|
|
||||||
strikeStyle: component.strikethrough ?? false,
|
|
||||||
text
|
|
||||||
})
|
|
||||||
for (const child of node.children) {
|
|
||||||
const stop = renderText(child) === false
|
|
||||||
if (stop) return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderText(rendered)
|
|
||||||
|
|
||||||
// skip rendering empty lines (and possible signs)
|
|
||||||
if (!plainText.trim()) continue
|
|
||||||
|
|
||||||
const ctx = getCtx()
|
|
||||||
const fontSize = 1.6 * factor
|
|
||||||
ctx.font = `${fontSize}px mojangles`
|
|
||||||
const textWidth = ctx.measureText(plainText).width
|
|
||||||
|
|
||||||
let renderedWidth = 0
|
|
||||||
for (const { fillStyle, fontStyle, strikeStyle, text, underlineStyle } of toRenderCanvas) {
|
|
||||||
// todo strikeStyle, underlineStyle
|
|
||||||
ctx.fillStyle = fillStyle
|
|
||||||
ctx.font = `${fontStyle} ${fontSize}px mojangles`
|
|
||||||
ctx.fillText(text, (canvas!.width - textWidth) / 2 + renderedWidth, fontSize * (lineNum + 1))
|
|
||||||
renderedWidth += ctx.measureText(text).width // todo isn't the font is monospace?
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// ctx.fillStyle = 'red'
|
|
||||||
// ctx.fillRect(0, 0, canvas.width, canvas.height)
|
|
||||||
|
|
||||||
return canvas
|
return canvas
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const renderComponent = (
|
||||||
|
text: JsonEncodedType | string | undefined,
|
||||||
|
PrismarineChat: typeof ChatMessage,
|
||||||
|
canvas: OffscreenCanvas,
|
||||||
|
fontSize: number,
|
||||||
|
defaultColor: string,
|
||||||
|
offset = 0
|
||||||
|
) => {
|
||||||
|
// todo: in pre flatenning it seems the format was not json
|
||||||
|
const parsed = typeof text === 'string' && (text?.startsWith('{') || text?.startsWith('"')) ? parseSafe(text ?? '""', 'sign text') : text
|
||||||
|
if (!parsed || (typeof parsed !== 'object' && typeof parsed !== 'string')) return
|
||||||
|
// todo fix type
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d')!
|
||||||
|
if (!ctx) throw new Error('Could not get 2d context')
|
||||||
|
ctx.imageSmoothingEnabled = false
|
||||||
|
ctx.font = `${fontSize}px mojangles`
|
||||||
|
|
||||||
|
type Formatting = {
|
||||||
|
color: string | undefined
|
||||||
|
underlined: boolean | undefined
|
||||||
|
strikethrough: boolean | undefined
|
||||||
|
bold: boolean | undefined
|
||||||
|
italic: boolean | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
type Message = ChatMessage & Formatting & { text: string }
|
||||||
|
|
||||||
|
const message = new PrismarineChat(parsed) as Message
|
||||||
|
|
||||||
|
const toRenderCanvas: Array<{
|
||||||
|
fontStyle: string
|
||||||
|
fillStyle: string
|
||||||
|
underlineStyle: boolean
|
||||||
|
strikeStyle: boolean
|
||||||
|
offset: number
|
||||||
|
text: string
|
||||||
|
}> = []
|
||||||
|
let visibleFormatting = false
|
||||||
|
let plainText = ''
|
||||||
|
let textOffset = offset
|
||||||
|
const textWidths: number[] = []
|
||||||
|
|
||||||
|
const renderText = (component: Message, parentFormatting?: Formatting | undefined) => {
|
||||||
|
const { text } = component
|
||||||
|
const formatting = {
|
||||||
|
color: component.color ?? parentFormatting?.color,
|
||||||
|
underlined: component.underlined ?? parentFormatting?.underlined,
|
||||||
|
strikethrough: component.strikethrough ?? parentFormatting?.strikethrough,
|
||||||
|
bold: component.bold ?? parentFormatting?.bold,
|
||||||
|
italic: component.italic ?? parentFormatting?.italic
|
||||||
|
}
|
||||||
|
visibleFormatting = visibleFormatting || formatting.underlined || formatting.strikethrough || false
|
||||||
|
if (text?.includes('\n')) {
|
||||||
|
for (const line of text.split('\n')) {
|
||||||
|
addTextPart(line, formatting)
|
||||||
|
textOffset += fontSize
|
||||||
|
plainText = ''
|
||||||
|
}
|
||||||
|
} else if (text) {
|
||||||
|
addTextPart(text, formatting)
|
||||||
|
}
|
||||||
|
if (component.extra) {
|
||||||
|
for (const child of component.extra) {
|
||||||
|
renderText(child as Message, formatting)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addTextPart = (text: string, formatting: Formatting) => {
|
||||||
|
plainText += text
|
||||||
|
textWidths[textOffset] = ctx.measureText(plainText).width
|
||||||
|
let color = formatting.color ?? defaultColor
|
||||||
|
if (!color.startsWith('#')) {
|
||||||
|
color = LEGACY_COLORS[color.toLowerCase()] || color
|
||||||
|
}
|
||||||
|
toRenderCanvas.push({
|
||||||
|
fontStyle: `${formatting.bold ? 'bold' : ''} ${formatting.italic ? 'italic' : ''}`,
|
||||||
|
fillStyle: color,
|
||||||
|
underlineStyle: formatting.underlined ?? false,
|
||||||
|
strikeStyle: formatting.strikethrough ?? false,
|
||||||
|
offset: textOffset,
|
||||||
|
text
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
renderText(message)
|
||||||
|
|
||||||
|
// skip rendering empty lines
|
||||||
|
if (!visibleFormatting && !message.toString().trim()) return
|
||||||
|
|
||||||
|
let renderedWidth = 0
|
||||||
|
let previousOffsetY = 0
|
||||||
|
for (const { fillStyle, fontStyle, underlineStyle, strikeStyle, offset: offsetY, text } of toRenderCanvas) {
|
||||||
|
if (previousOffsetY !== offsetY) {
|
||||||
|
renderedWidth = 0
|
||||||
|
}
|
||||||
|
previousOffsetY = offsetY
|
||||||
|
ctx.fillStyle = fillStyle
|
||||||
|
ctx.textRendering = 'optimizeLegibility'
|
||||||
|
ctx.font = `${fontStyle} ${fontSize}px mojangles`
|
||||||
|
const textWidth = textWidths[offsetY] ?? ctx.measureText(text).width
|
||||||
|
const offsetX = (canvas.width - textWidth) / 2 + renderedWidth
|
||||||
|
ctx.fillText(text, offsetX, offsetY)
|
||||||
|
if (strikeStyle) {
|
||||||
|
ctx.lineWidth = fontSize / 8
|
||||||
|
ctx.strokeStyle = fillStyle
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(offsetX, offsetY - ctx.lineWidth * 2.5)
|
||||||
|
ctx.lineTo(offsetX + ctx.measureText(text).width, offsetY - ctx.lineWidth * 2.5)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
if (underlineStyle) {
|
||||||
|
ctx.lineWidth = fontSize / 8
|
||||||
|
ctx.strokeStyle = fillStyle
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(offsetX, offsetY + ctx.lineWidth)
|
||||||
|
ctx.lineTo(offsetX + ctx.measureText(text).width, offsetY + ctx.lineWidth)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
renderedWidth += ctx.measureText(text).width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,14 @@ const blockEntity = {
|
||||||
|
|
||||||
await document.fonts.load('1em mojangles')
|
await document.fonts.load('1em mojangles')
|
||||||
|
|
||||||
const canvas = renderSign(blockEntity, PrismarineChat, (ctx) => {
|
const canvas = renderSign(blockEntity, false, PrismarineChat, (ctx) => {
|
||||||
ctx.drawImage(img, 0, 0, ctx.canvas.width, ctx.canvas.height)
|
ctx.drawImage(img, 0, 0, ctx.canvas.width, ctx.canvas.height)
|
||||||
})
|
}, (width, height) => {
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = width
|
||||||
|
canvas.height = height
|
||||||
|
return canvas as unknown as OffscreenCanvas
|
||||||
|
}) as unknown as HTMLCanvasElement
|
||||||
|
|
||||||
if (canvas) {
|
if (canvas) {
|
||||||
canvas.style.imageRendering = 'pixelated'
|
canvas.style.imageRendering = 'pixelated'
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ global.document = {
|
||||||
|
|
||||||
const render = (entity) => {
|
const render = (entity) => {
|
||||||
ctxTexts = []
|
ctxTexts = []
|
||||||
renderSign(entity, PrismarineChat)
|
renderSign(entity, true, PrismarineChat)
|
||||||
return ctxTexts.map(({ text, y }) => [y / 64, text])
|
return ctxTexts.map(({ text, y }) => [y / 64, text])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,10 +37,6 @@ test('sign renderer', () => {
|
||||||
} as any
|
} as any
|
||||||
expect(render(blockEntity)).toMatchInlineSnapshot(`
|
expect(render(blockEntity)).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
[
|
|
||||||
1,
|
|
||||||
"",
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
1,
|
1,
|
||||||
"Minecraft ",
|
"Minecraft ",
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import { BlockModel } from 'mc-assets/dist/types'
|
import { BlockModel } from 'mc-assets/dist/types'
|
||||||
import { ItemSpecificContextProperties } from 'renderer/viewer/lib/basePlayerState'
|
import { ItemSpecificContextProperties, PlayerStateRenderer } from 'renderer/viewer/lib/basePlayerState'
|
||||||
import { renderSlot } from '../../../src/inventoryWindows'
|
|
||||||
import { GeneralInputItem, getItemModelName } from '../../../src/mineflayer/items'
|
import { GeneralInputItem, getItemModelName } from '../../../src/mineflayer/items'
|
||||||
import { ResourcesManager } from '../../../src/resourcesManager'
|
import { ResourcesManager, ResourcesManagerTransferred } from '../../../src/resourcesManager'
|
||||||
|
import { renderSlot } from './renderSlot'
|
||||||
|
|
||||||
export const getItemUv = (item: Record<string, any>, specificProps: ItemSpecificContextProperties, resourcesManager: ResourcesManager): {
|
export const getItemUv = (item: Record<string, any>, specificProps: ItemSpecificContextProperties, resourcesManager: ResourcesManagerTransferred, playerState: PlayerStateRenderer): {
|
||||||
u: number
|
u: number
|
||||||
v: number
|
v: number
|
||||||
su: number
|
su: number
|
||||||
sv: number
|
sv: number
|
||||||
renderInfo?: ReturnType<typeof renderSlot>
|
renderInfo?: ReturnType<typeof renderSlot>
|
||||||
texture: HTMLImageElement
|
// texture: ImageBitmap
|
||||||
modelName: string
|
modelName: string
|
||||||
} | {
|
} | {
|
||||||
resolvedModel: BlockModel
|
resolvedModel: BlockModel
|
||||||
|
|
@ -30,11 +30,11 @@ export const getItemUv = (item: Record<string, any>, specificProps: ItemSpecific
|
||||||
const model = getItemModelName({
|
const model = getItemModelName({
|
||||||
...item,
|
...item,
|
||||||
name,
|
name,
|
||||||
} as GeneralInputItem, specificProps, resourcesManager)
|
} as GeneralInputItem, specificProps, resourcesManager, playerState)
|
||||||
|
|
||||||
const renderInfo = renderSlot({
|
const renderInfo = renderSlot({
|
||||||
modelName: model,
|
modelName: model,
|
||||||
}, false, true)
|
}, resourcesManager, false, true)
|
||||||
|
|
||||||
if (!renderInfo) throw new Error(`Failed to get render info for item ${name}`)
|
if (!renderInfo) throw new Error(`Failed to get render info for item ${name}`)
|
||||||
|
|
||||||
|
|
@ -53,7 +53,7 @@ export const getItemUv = (item: Record<string, any>, specificProps: ItemSpecific
|
||||||
return {
|
return {
|
||||||
u, v, su, sv,
|
u, v, su, sv,
|
||||||
renderInfo,
|
renderInfo,
|
||||||
texture: img,
|
// texture: img,
|
||||||
modelName: renderInfo.modelName!
|
modelName: renderInfo.modelName!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +67,7 @@ export const getItemUv = (item: Record<string, any>, specificProps: ItemSpecific
|
||||||
v: 0,
|
v: 0,
|
||||||
su: 16 / resources.blocksAtlasImage.width,
|
su: 16 / resources.blocksAtlasImage.width,
|
||||||
sv: 16 / resources.blocksAtlasImage.width,
|
sv: 16 / resources.blocksAtlasImage.width,
|
||||||
texture: resources.blocksAtlasImage,
|
// texture: resources.blocksAtlasImage,
|
||||||
modelName: 'missing'
|
modelName: 'missing'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,10 @@ export class CameraShake {
|
||||||
this.update()
|
this.update()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBaseRotation () {
|
||||||
|
return { pitch: this.basePitch, yaw: this.baseYaw }
|
||||||
|
}
|
||||||
|
|
||||||
shakeFromDamage (yaw?: number) {
|
shakeFromDamage (yaw?: number) {
|
||||||
// Add roll animation
|
// Add roll animation
|
||||||
const startRoll = this.rollAngle
|
const startRoll = this.rollAngle
|
||||||
|
|
@ -35,6 +39,11 @@ export class CameraShake {
|
||||||
}
|
}
|
||||||
|
|
||||||
update () {
|
update () {
|
||||||
|
if (this.worldRenderer.playerStateUtils.isSpectatingEntity()) {
|
||||||
|
// Remove any shaking when spectating
|
||||||
|
this.rollAngle = 0
|
||||||
|
this.rollAnimation = undefined
|
||||||
|
}
|
||||||
// Update roll animation
|
// Update roll animation
|
||||||
if (this.rollAnimation) {
|
if (this.rollAnimation) {
|
||||||
const now = performance.now()
|
const now = performance.now()
|
||||||
|
|
@ -63,7 +72,7 @@ export class CameraShake {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const camera = this.worldRenderer.cameraGroupVr || this.worldRenderer.camera
|
const camera = this.worldRenderer.cameraObject
|
||||||
|
|
||||||
if (this.worldRenderer.cameraGroupVr) {
|
if (this.worldRenderer.cameraGroupVr) {
|
||||||
// For VR camera, only apply yaw rotation
|
// For VR camera, only apply yaw rotation
|
||||||
|
|
@ -71,8 +80,12 @@ export class CameraShake {
|
||||||
camera.setRotationFromQuaternion(yawQuat)
|
camera.setRotationFromQuaternion(yawQuat)
|
||||||
} else {
|
} else {
|
||||||
// For regular camera, apply all rotations
|
// For regular camera, apply all rotations
|
||||||
const pitchQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), this.basePitch)
|
// Add tiny offsets to prevent z-fighting at ideal angles (90, 180, 270 degrees)
|
||||||
const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), this.baseYaw)
|
const pitchOffset = this.addAntiZfightingOffset(this.basePitch)
|
||||||
|
const yawOffset = this.addAntiZfightingOffset(this.baseYaw)
|
||||||
|
|
||||||
|
const pitchQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), pitchOffset)
|
||||||
|
const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), yawOffset)
|
||||||
const rollQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), THREE.MathUtils.degToRad(this.rollAngle))
|
const rollQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), THREE.MathUtils.degToRad(this.rollAngle))
|
||||||
// Combine rotations in the correct order: pitch -> yaw -> roll
|
// Combine rotations in the correct order: pitch -> yaw -> roll
|
||||||
const finalQuat = yawQuat.multiply(pitchQuat).multiply(rollQuat)
|
const finalQuat = yawQuat.multiply(pitchQuat).multiply(rollQuat)
|
||||||
|
|
@ -87,4 +100,21 @@ export class CameraShake {
|
||||||
private easeInOut (t: number): number {
|
private easeInOut (t: number): number {
|
||||||
return t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2
|
return t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private addAntiZfightingOffset (angle: number): number {
|
||||||
|
const offset = 0.001 // Very small offset in radians (about 0.057 degrees)
|
||||||
|
|
||||||
|
// Check if the angle is close to ideal angles (0, π/2, π, 3π/2)
|
||||||
|
const normalizedAngle = ((angle % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2)
|
||||||
|
const tolerance = 0.01 // Tolerance for considering an angle "ideal"
|
||||||
|
|
||||||
|
if (Math.abs(normalizedAngle) < tolerance ||
|
||||||
|
Math.abs(normalizedAngle - Math.PI / 2) < tolerance ||
|
||||||
|
Math.abs(normalizedAngle - Math.PI) < tolerance ||
|
||||||
|
Math.abs(normalizedAngle - 3 * Math.PI / 2) < tolerance) {
|
||||||
|
return angle + offset
|
||||||
|
}
|
||||||
|
|
||||||
|
return angle
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,20 @@ import { GraphicsBackendConfig, GraphicsInitOptions } from '../../../src/appView
|
||||||
import { WorldRendererConfig } from '../lib/worldrendererCommon'
|
import { WorldRendererConfig } from '../lib/worldrendererCommon'
|
||||||
|
|
||||||
export class DocumentRenderer {
|
export class DocumentRenderer {
|
||||||
readonly canvas = document.createElement('canvas')
|
canvas: HTMLCanvasElement | OffscreenCanvas
|
||||||
readonly renderer: THREE.WebGLRenderer
|
readonly renderer: THREE.WebGLRenderer
|
||||||
private animationFrameId?: number
|
private animationFrameId?: number
|
||||||
|
private timeoutId?: number
|
||||||
private lastRenderTime = 0
|
private lastRenderTime = 0
|
||||||
private previousWindowWidth = window.innerWidth
|
|
||||||
private previousWindowHeight = window.innerHeight
|
private previousCanvasWidth = 0
|
||||||
|
private previousCanvasHeight = 0
|
||||||
|
private currentWidth = 0
|
||||||
|
private currentHeight = 0
|
||||||
|
|
||||||
private renderedFps = 0
|
private renderedFps = 0
|
||||||
private fpsInterval: any
|
private fpsInterval: any
|
||||||
private readonly stats: TopRightStats
|
private readonly stats: TopRightStats | undefined
|
||||||
private paused = false
|
private paused = false
|
||||||
disconnected = false
|
disconnected = false
|
||||||
preRender = () => { }
|
preRender = () => { }
|
||||||
|
|
@ -26,9 +31,16 @@ export class DocumentRenderer {
|
||||||
onRender = [] as Array<(sizeChanged: boolean) => void>
|
onRender = [] as Array<(sizeChanged: boolean) => void>
|
||||||
inWorldRenderingConfig: WorldRendererConfig | undefined
|
inWorldRenderingConfig: WorldRendererConfig | undefined
|
||||||
|
|
||||||
constructor (initOptions: GraphicsInitOptions) {
|
constructor (initOptions: GraphicsInitOptions, public externalCanvas?: OffscreenCanvas) {
|
||||||
this.config = initOptions.config
|
this.config = initOptions.config
|
||||||
|
|
||||||
|
// Handle canvas creation/transfer based on context
|
||||||
|
if (externalCanvas) {
|
||||||
|
this.canvas = externalCanvas
|
||||||
|
} else {
|
||||||
|
this.addToPage()
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.renderer = new THREE.WebGLRenderer({
|
this.renderer = new THREE.WebGLRenderer({
|
||||||
canvas: this.canvas,
|
canvas: this.canvas,
|
||||||
|
|
@ -37,17 +49,25 @@ export class DocumentRenderer {
|
||||||
powerPreference: this.config.powerPreference
|
powerPreference: this.config.powerPreference
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
initOptions.displayCriticalError(new Error(`Failed to create WebGL context, not possible to render (restart browser): ${err.message}`))
|
initOptions.callbacks.displayCriticalError(new Error(`Failed to create WebGL context, not possible to render (restart browser): ${err.message}`))
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace
|
this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace
|
||||||
this.updatePixelRatio()
|
if (!externalCanvas) {
|
||||||
this.updateSize()
|
this.updatePixelRatio()
|
||||||
this.addToPage()
|
}
|
||||||
|
this.sizeUpdated()
|
||||||
|
// Initialize previous dimensions
|
||||||
|
this.previousCanvasWidth = this.canvas.width
|
||||||
|
this.previousCanvasHeight = this.canvas.height
|
||||||
|
|
||||||
this.stats = new TopRightStats(this.canvas, this.config.statsVisible)
|
const supportsWebGL2 = 'WebGL2RenderingContext' in window
|
||||||
|
// Only initialize stats and DOM-related features in main thread
|
||||||
|
if (!externalCanvas && supportsWebGL2) {
|
||||||
|
this.stats = new TopRightStats(this.canvas as HTMLCanvasElement, this.config.statsVisible)
|
||||||
|
this.setupFpsTracking()
|
||||||
|
}
|
||||||
|
|
||||||
this.setupFpsTracking()
|
|
||||||
this.startRenderLoop()
|
this.startRenderLoop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,15 +79,33 @@ export class DocumentRenderer {
|
||||||
this.renderer.setPixelRatio(pixelRatio)
|
this.renderer.setPixelRatio(pixelRatio)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSize () {
|
sizeUpdated () {
|
||||||
this.renderer.setSize(window.innerWidth, window.innerHeight)
|
this.renderer.setSize(this.currentWidth, this.currentHeight, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private addToPage () {
|
private addToPage () {
|
||||||
this.canvas.id = 'viewer-canvas'
|
this.canvas = addCanvasToPage()
|
||||||
this.canvas.style.width = '100%'
|
this.updateCanvasSize()
|
||||||
this.canvas.style.height = '100%'
|
}
|
||||||
document.body.appendChild(this.canvas)
|
|
||||||
|
updateSizeExternal (newWidth: number, newHeight: number, pixelRatio: number) {
|
||||||
|
this.currentWidth = newWidth
|
||||||
|
this.currentHeight = newHeight
|
||||||
|
this.renderer.setPixelRatio(pixelRatio)
|
||||||
|
this.sizeUpdated()
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateCanvasSize () {
|
||||||
|
if (!this.externalCanvas) {
|
||||||
|
const innnerWidth = window.innerWidth
|
||||||
|
const innnerHeight = window.innerHeight
|
||||||
|
if (this.currentWidth !== innnerWidth) {
|
||||||
|
this.currentWidth = innnerWidth
|
||||||
|
}
|
||||||
|
if (this.currentHeight !== innnerHeight) {
|
||||||
|
this.currentHeight = innnerHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupFpsTracking () {
|
private setupFpsTracking () {
|
||||||
|
|
@ -81,20 +119,15 @@ export class DocumentRenderer {
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// private handleResize () {
|
|
||||||
// const width = window.innerWidth
|
|
||||||
// const height = window.innerHeight
|
|
||||||
|
|
||||||
// viewer.camera.aspect = width / height
|
|
||||||
// viewer.camera.updateProjectionMatrix()
|
|
||||||
// this.renderer.setSize(width, height)
|
|
||||||
// viewer.world.handleResize()
|
|
||||||
// }
|
|
||||||
|
|
||||||
private startRenderLoop () {
|
private startRenderLoop () {
|
||||||
const animate = () => {
|
const animate = () => {
|
||||||
if (this.disconnected) return
|
if (this.disconnected) return
|
||||||
this.animationFrameId = requestAnimationFrame(animate)
|
|
||||||
|
if (this.config.timeoutRendering) {
|
||||||
|
this.timeoutId = setTimeout(animate, this.config.fpsLimit ? 1000 / this.config.fpsLimit : 0) as unknown as number
|
||||||
|
} else {
|
||||||
|
this.animationFrameId = requestAnimationFrame(animate)
|
||||||
|
}
|
||||||
|
|
||||||
if (this.paused || (this.renderer.xr.isPresenting && !this.inWorldRenderingConfig?.vrPageGameRendering)) return
|
if (this.paused || (this.renderer.xr.isPresenting && !this.inWorldRenderingConfig?.vrPageGameRendering)) return
|
||||||
|
|
||||||
|
|
@ -112,18 +145,19 @@ export class DocumentRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
let sizeChanged = false
|
let sizeChanged = false
|
||||||
if (this.previousWindowWidth !== window.innerWidth || this.previousWindowHeight !== window.innerHeight) {
|
this.updateCanvasSize()
|
||||||
this.previousWindowWidth = window.innerWidth
|
if (this.previousCanvasWidth !== this.currentWidth || this.previousCanvasHeight !== this.currentHeight) {
|
||||||
this.previousWindowHeight = window.innerHeight
|
this.previousCanvasWidth = this.currentWidth
|
||||||
this.updateSize()
|
this.previousCanvasHeight = this.currentHeight
|
||||||
|
this.sizeUpdated()
|
||||||
sizeChanged = true
|
sizeChanged = true
|
||||||
}
|
}
|
||||||
|
|
||||||
this.frameRender(sizeChanged)
|
this.frameRender(sizeChanged)
|
||||||
|
|
||||||
// Update stats visibility each frame
|
// Update stats visibility each frame (main thread only)
|
||||||
if (this.config.statsVisible !== undefined) {
|
if (this.config.statsVisible !== undefined) {
|
||||||
this.stats.setVisibility(this.config.statsVisible)
|
this.stats?.setVisibility(this.config.statsVisible)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -132,16 +166,16 @@ export class DocumentRenderer {
|
||||||
|
|
||||||
frameRender (sizeChanged: boolean) {
|
frameRender (sizeChanged: boolean) {
|
||||||
this.preRender()
|
this.preRender()
|
||||||
this.stats.markStart()
|
this.stats?.markStart()
|
||||||
tween.update()
|
tween.update()
|
||||||
if (!window.freezeRender) {
|
if (!globalThis.freezeRender) {
|
||||||
this.render(sizeChanged)
|
this.render(sizeChanged)
|
||||||
}
|
}
|
||||||
for (const fn of this.onRender) {
|
for (const fn of this.onRender) {
|
||||||
fn(sizeChanged)
|
fn(sizeChanged)
|
||||||
}
|
}
|
||||||
this.renderedFps++
|
this.renderedFps++
|
||||||
this.stats.markEnd()
|
this.stats?.markEnd()
|
||||||
this.postRender()
|
this.postRender()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,10 +188,15 @@ export class DocumentRenderer {
|
||||||
if (this.animationFrameId) {
|
if (this.animationFrameId) {
|
||||||
cancelAnimationFrame(this.animationFrameId)
|
cancelAnimationFrame(this.animationFrameId)
|
||||||
}
|
}
|
||||||
this.canvas.remove()
|
if (this.timeoutId) {
|
||||||
this.renderer.dispose()
|
clearTimeout(this.timeoutId)
|
||||||
|
}
|
||||||
|
if (this.canvas instanceof HTMLCanvasElement) {
|
||||||
|
this.canvas.remove()
|
||||||
|
}
|
||||||
clearInterval(this.fpsInterval)
|
clearInterval(this.fpsInterval)
|
||||||
this.stats.dispose()
|
this.stats?.dispose()
|
||||||
|
this.renderer.dispose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -250,3 +289,40 @@ class TopRightStats {
|
||||||
this.statsGl.container.remove()
|
this.statsGl.container.remove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addCanvasToPage = () => {
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.id = 'viewer-canvas'
|
||||||
|
document.body.appendChild(canvas)
|
||||||
|
return canvas
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addCanvasForWorker = () => {
|
||||||
|
const canvas = addCanvasToPage()
|
||||||
|
const transferred = canvas.transferControlToOffscreen()
|
||||||
|
let removed = false
|
||||||
|
let onSizeChanged = (w, h) => { }
|
||||||
|
let oldSize = { width: 0, height: 0 }
|
||||||
|
const checkSize = () => {
|
||||||
|
if (removed) return
|
||||||
|
if (oldSize.width !== window.innerWidth || oldSize.height !== window.innerHeight) {
|
||||||
|
onSizeChanged(window.innerWidth, window.innerHeight)
|
||||||
|
oldSize = { width: window.innerWidth, height: window.innerHeight }
|
||||||
|
}
|
||||||
|
requestAnimationFrame(checkSize)
|
||||||
|
}
|
||||||
|
requestAnimationFrame(checkSize)
|
||||||
|
return {
|
||||||
|
canvas: transferred,
|
||||||
|
destroy () {
|
||||||
|
removed = true
|
||||||
|
canvas.remove()
|
||||||
|
},
|
||||||
|
onSizeChanged (cb: (width: number, height: number) => void) {
|
||||||
|
onSizeChanged = cb
|
||||||
|
},
|
||||||
|
get size () {
|
||||||
|
return { width: window.innerWidth, height: window.innerHeight }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
//@ts-check
|
//@ts-check
|
||||||
import EventEmitter from 'events'
|
|
||||||
import { UnionToIntersection } from 'type-fest'
|
import { UnionToIntersection } from 'type-fest'
|
||||||
import nbt from 'prismarine-nbt'
|
import nbt from 'prismarine-nbt'
|
||||||
import * as TWEEN from '@tweenjs/tween.js'
|
import * as TWEEN from '@tweenjs/tween.js'
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
import { PlayerObject, PlayerAnimation } from 'skinview3d'
|
import { PlayerAnimation, PlayerObject } from 'skinview3d'
|
||||||
import { loadSkinToCanvas, loadEarsToCanvasFromSkin, inferModelType, loadCapeToCanvas, loadImage } from 'skinview-utils'
|
import { inferModelType, loadCapeToCanvas, loadEarsToCanvasFromSkin } from 'skinview-utils'
|
||||||
// todo replace with url
|
// todo replace with url
|
||||||
import { degreesToRadians } from '@nxg-org/mineflayer-tracker/lib/mathUtils'
|
import { degreesToRadians } from '@nxg-org/mineflayer-tracker/lib/mathUtils'
|
||||||
import { NameTagObject } from 'skinview3d/libs/nametag'
|
import { NameTagObject } from 'skinview3d/libs/nametag'
|
||||||
|
|
@ -13,28 +12,27 @@ import { flat, fromFormattedString } from '@xmcl/text-component'
|
||||||
import mojangson from 'mojangson'
|
import mojangson from 'mojangson'
|
||||||
import { snakeCase } from 'change-case'
|
import { snakeCase } from 'change-case'
|
||||||
import { Item } from 'prismarine-item'
|
import { Item } from 'prismarine-item'
|
||||||
import { BlockModel } from 'mc-assets'
|
|
||||||
import { isEntityAttackable } from 'mineflayer-mouse/dist/attackableEntity'
|
import { isEntityAttackable } from 'mineflayer-mouse/dist/attackableEntity'
|
||||||
import { Vec3 } from 'vec3'
|
import { Team } from 'mineflayer'
|
||||||
|
import PrismarineChatLoader from 'prismarine-chat'
|
||||||
import { EntityMetadataVersions } from '../../../src/mcDataTypes'
|
import { EntityMetadataVersions } from '../../../src/mcDataTypes'
|
||||||
import { ItemSpecificContextProperties } from '../lib/basePlayerState'
|
import { ItemSpecificContextProperties } from '../lib/basePlayerState'
|
||||||
import { loadSkinImage, loadSkinFromUsername, stevePngUrl, steveTexture } from '../lib/utils/skins'
|
import { loadSkinFromUsername, loadSkinImage, stevePngUrl } from '../lib/utils/skins'
|
||||||
import { loadTexture } from '../lib/utils'
|
import { renderComponent } from '../sign-renderer'
|
||||||
|
import { createCanvas } from '../lib/utils'
|
||||||
|
import { PlayerObjectType } from '../lib/createPlayerObject'
|
||||||
import { getBlockMeshFromModel } from './holdingBlock'
|
import { getBlockMeshFromModel } from './holdingBlock'
|
||||||
|
import { createItemMesh } from './itemMesh'
|
||||||
import * as Entity from './entity/EntityMesh'
|
import * as Entity from './entity/EntityMesh'
|
||||||
import { getMesh } from './entity/EntityMesh'
|
import { getMesh } from './entity/EntityMesh'
|
||||||
import { WalkingGeneralSwing } from './entity/animations'
|
import { WalkingGeneralSwing } from './entity/animations'
|
||||||
import { disposeObject } from './threeJsUtils'
|
import { disposeObject, loadTexture, loadThreeJsTextureFromUrl } from './threeJsUtils'
|
||||||
import { armorModel, armorTextures } from './entity/armorModels'
|
import { armorModel, armorTextures, elytraTexture } from './entity/armorModels'
|
||||||
import { WorldRendererThree } from './worldrendererThree'
|
import { WorldRendererThree } from './worldrendererThree'
|
||||||
|
|
||||||
export const TWEEN_DURATION = 120
|
export const steveTexture = loadThreeJsTextureFromUrl(stevePngUrl)
|
||||||
|
|
||||||
type PlayerObjectType = PlayerObject & {
|
export const TWEEN_DURATION = 120
|
||||||
animation?: PlayerAnimation
|
|
||||||
realPlayerUuid: string
|
|
||||||
realUsername: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function convert2sComplementToHex (complement: number) {
|
function convert2sComplementToHex (complement: number) {
|
||||||
if (complement < 0) {
|
if (complement < 0) {
|
||||||
|
|
@ -95,8 +93,11 @@ function getUsernameTexture ({
|
||||||
username,
|
username,
|
||||||
nameTagBackgroundColor = 'rgba(0, 0, 0, 0.3)',
|
nameTagBackgroundColor = 'rgba(0, 0, 0, 0.3)',
|
||||||
nameTagTextOpacity = 255
|
nameTagTextOpacity = 255
|
||||||
}: any, { fontFamily = 'sans-serif' }: any) {
|
}: any, { fontFamily = 'mojangles' }: any, version: string) {
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = createCanvas(64, 64)
|
||||||
|
|
||||||
|
const PrismarineChat = PrismarineChatLoader(version)
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d')
|
const ctx = canvas.getContext('2d')
|
||||||
if (!ctx) throw new Error('Could not get 2d context')
|
if (!ctx) throw new Error('Could not get 2d context')
|
||||||
|
|
||||||
|
|
@ -104,38 +105,39 @@ function getUsernameTexture ({
|
||||||
const padding = 5
|
const padding = 5
|
||||||
ctx.font = `${fontSize}px ${fontFamily}`
|
ctx.font = `${fontSize}px ${fontFamily}`
|
||||||
|
|
||||||
const lines = String(username).split('\n')
|
const plainLines = String(typeof username === 'string' ? username : new PrismarineChat(username).toString()).split('\n')
|
||||||
|
|
||||||
let textWidth = 0
|
let textWidth = 0
|
||||||
for (const line of lines) {
|
for (const line of plainLines) {
|
||||||
const width = ctx.measureText(line).width + padding * 2
|
const width = ctx.measureText(line).width + padding * 2
|
||||||
if (width > textWidth) textWidth = width
|
if (width > textWidth) textWidth = width
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas.width = textWidth
|
canvas.width = textWidth
|
||||||
canvas.height = (fontSize + padding) * lines.length
|
canvas.height = (fontSize + padding) * plainLines.length
|
||||||
|
|
||||||
ctx.fillStyle = nameTagBackgroundColor
|
ctx.fillStyle = nameTagBackgroundColor
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
ctx.font = `${fontSize}px ${fontFamily}`
|
ctx.globalAlpha = nameTagTextOpacity / 255
|
||||||
ctx.fillStyle = `rgba(255, 255, 255, ${nameTagTextOpacity / 255})`
|
|
||||||
let i = 0
|
renderComponent(username, PrismarineChat, canvas, fontSize, 'white', -padding + fontSize)
|
||||||
for (const line of lines) {
|
|
||||||
i++
|
ctx.globalAlpha = 1
|
||||||
ctx.fillText(line, (textWidth - ctx.measureText(line).width) / 2, -padding + fontSize * i)
|
|
||||||
}
|
|
||||||
|
|
||||||
return canvas
|
return canvas
|
||||||
}
|
}
|
||||||
|
|
||||||
const addNametag = (entity, options, mesh) => {
|
const addNametag = (entity, options: { fontFamily: string }, mesh, version: string) => {
|
||||||
|
for (const c of mesh.children) {
|
||||||
|
if (c.name === 'nametag') {
|
||||||
|
c.removeFromParent()
|
||||||
|
}
|
||||||
|
}
|
||||||
if (entity.username !== undefined) {
|
if (entity.username !== undefined) {
|
||||||
if (mesh.children.some(c => c.name === 'nametag')) return // todo update
|
const canvas = getUsernameTexture(entity, options, version)
|
||||||
const canvas = getUsernameTexture(entity, options)
|
|
||||||
const tex = new THREE.Texture(canvas)
|
const tex = new THREE.Texture(canvas)
|
||||||
tex.needsUpdate = true
|
tex.needsUpdate = true
|
||||||
let nameTag
|
let nameTag: THREE.Object3D
|
||||||
if (entity.nameTagFixed) {
|
if (entity.nameTagFixed) {
|
||||||
const geometry = new THREE.PlaneGeometry()
|
const geometry = new THREE.PlaneGeometry()
|
||||||
const material = new THREE.MeshBasicMaterial({ map: tex })
|
const material = new THREE.MeshBasicMaterial({ map: tex })
|
||||||
|
|
@ -165,6 +167,7 @@ const addNametag = (entity, options, mesh) => {
|
||||||
nameTag.name = 'nametag'
|
nameTag.name = 'nametag'
|
||||||
|
|
||||||
mesh.add(nameTag)
|
mesh.add(nameTag)
|
||||||
|
return nameTag
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -173,7 +176,7 @@ const nametags = {}
|
||||||
|
|
||||||
const isFirstUpperCase = (str) => str.charAt(0) === str.charAt(0).toUpperCase()
|
const isFirstUpperCase = (str) => str.charAt(0) === str.charAt(0).toUpperCase()
|
||||||
|
|
||||||
function getEntityMesh (entity: import('prismarine-entity').Entity & { delete?: any; pos: any; name: any }, world: WorldRendererThree | undefined, options: { fontFamily: string }, overrides) {
|
function getEntityMesh (entity: import('prismarine-entity').Entity & { delete?: any; pos?: any; name?: any }, world: WorldRendererThree, options: { fontFamily: string }, overrides) {
|
||||||
if (entity.name) {
|
if (entity.name) {
|
||||||
try {
|
try {
|
||||||
// https://github.com/PrismarineJS/prismarine-viewer/pull/410
|
// https://github.com/PrismarineJS/prismarine-viewer/pull/410
|
||||||
|
|
@ -181,7 +184,7 @@ function getEntityMesh (entity: import('prismarine-entity').Entity & { delete?:
|
||||||
const e = new Entity.EntityMesh('1.16.4', entityName, world, overrides)
|
const e = new Entity.EntityMesh('1.16.4', entityName, world, overrides)
|
||||||
|
|
||||||
if (e.mesh) {
|
if (e.mesh) {
|
||||||
addNametag(entity, options, e.mesh)
|
addNametag(entity, options, e.mesh, world.version)
|
||||||
return e.mesh
|
return e.mesh
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -199,7 +202,7 @@ function getEntityMesh (entity: import('prismarine-entity').Entity & { delete?:
|
||||||
addNametag({
|
addNametag({
|
||||||
username: entity.name,
|
username: entity.name,
|
||||||
height: entity.height,
|
height: entity.height,
|
||||||
}, options, cube)
|
}, options, cube, world.version)
|
||||||
}
|
}
|
||||||
return cube
|
return cube
|
||||||
}
|
}
|
||||||
|
|
@ -209,10 +212,12 @@ export type SceneEntity = THREE.Object3D & {
|
||||||
username?: string
|
username?: string
|
||||||
uuid?: string
|
uuid?: string
|
||||||
additionalCleanup?: () => void
|
additionalCleanup?: () => void
|
||||||
|
originalEntity: import('prismarine-entity').Entity & { delete?; pos?, name, team?: Team }
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Entities {
|
export class Entities {
|
||||||
entities = {} as Record<string, SceneEntity>
|
entities = {} as Record<string, SceneEntity>
|
||||||
|
playerEntity: SceneEntity | null = null // Special entity for the player in third person
|
||||||
entitiesOptions = {
|
entitiesOptions = {
|
||||||
fontFamily: 'mojangles'
|
fontFamily: 'mojangles'
|
||||||
}
|
}
|
||||||
|
|
@ -250,6 +255,37 @@ export class Entities {
|
||||||
constructor (public worldRenderer: WorldRendererThree) {
|
constructor (public worldRenderer: WorldRendererThree) {
|
||||||
this.debugMode = 'none'
|
this.debugMode = 'none'
|
||||||
this.onSkinUpdate = () => { }
|
this.onSkinUpdate = () => { }
|
||||||
|
this.watchResourcesUpdates()
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePlayerEntity (playerData: SceneEntity['originalEntity']) {
|
||||||
|
// Create player entity if it doesn't exist
|
||||||
|
if (!this.playerEntity) {
|
||||||
|
// Create the player entity similar to how normal entities are created
|
||||||
|
const group = new THREE.Group() as unknown as SceneEntity
|
||||||
|
group.originalEntity = { ...playerData, name: 'player' } as SceneEntity['originalEntity']
|
||||||
|
|
||||||
|
const wrapper = new THREE.Group()
|
||||||
|
const playerObject = this.setupPlayerObject(playerData, wrapper, {})
|
||||||
|
group.playerObject = playerObject
|
||||||
|
group.add(wrapper)
|
||||||
|
|
||||||
|
group.name = 'player_entity'
|
||||||
|
this.playerEntity = group
|
||||||
|
this.worldRenderer.scene.add(group)
|
||||||
|
|
||||||
|
void this.updatePlayerSkin(playerData.id, playerData.username, playerData.uuid ?? undefined, stevePngUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update position and rotation
|
||||||
|
if (playerData.position) {
|
||||||
|
this.playerEntity.position.set(playerData.position.x, playerData.position.y, playerData.position.z)
|
||||||
|
}
|
||||||
|
if (playerData.yaw !== undefined) {
|
||||||
|
this.playerEntity.rotation.y = playerData.yaw
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateEntityEquipment(this.playerEntity, playerData)
|
||||||
}
|
}
|
||||||
|
|
||||||
clear () {
|
clear () {
|
||||||
|
|
@ -258,6 +294,27 @@ export class Entities {
|
||||||
disposeObject(mesh)
|
disposeObject(mesh)
|
||||||
}
|
}
|
||||||
this.entities = {}
|
this.entities = {}
|
||||||
|
|
||||||
|
// Clean up player entity
|
||||||
|
if (this.playerEntity) {
|
||||||
|
this.worldRenderer.scene.remove(this.playerEntity)
|
||||||
|
disposeObject(this.playerEntity)
|
||||||
|
this.playerEntity = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadEntities () {
|
||||||
|
for (const entity of Object.values(this.entities)) {
|
||||||
|
// update all entities textures like held items, armour, etc
|
||||||
|
// todo update entity textures itself
|
||||||
|
this.update({ ...entity.originalEntity, delete: true, } as SceneEntity['originalEntity'], {})
|
||||||
|
this.update(entity.originalEntity, {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watchResourcesUpdates () {
|
||||||
|
this.worldRenderer.resourcesManager.on('assetsTexturesUpdated', () => this.reloadEntities())
|
||||||
|
this.worldRenderer.resourcesManager.on('assetsInventoryReady', () => this.reloadEntities())
|
||||||
}
|
}
|
||||||
|
|
||||||
setDebugMode (mode: string, entity: THREE.Object3D | null = null) {
|
setDebugMode (mode: string, entity: THREE.Object3D | null = null) {
|
||||||
|
|
@ -290,11 +347,12 @@ export class Entities {
|
||||||
}
|
}
|
||||||
|
|
||||||
const dt = this.clock.getDelta()
|
const dt = this.clock.getDelta()
|
||||||
const botPos = this.worldRenderer.viewerPosition
|
const botPos = this.worldRenderer.viewerChunkPosition
|
||||||
const VISIBLE_DISTANCE = 8 * 8
|
const VISIBLE_DISTANCE = 10 * 10
|
||||||
|
|
||||||
for (const entityId of Object.keys(this.entities)) {
|
// Update regular entities
|
||||||
const entity = this.entities[entityId]
|
for (const [entityId, entity] of [...Object.entries(this.entities), ['player_entity', this.playerEntity] as [string, SceneEntity | null]]) {
|
||||||
|
if (!entity) continue
|
||||||
const { playerObject } = entity
|
const { playerObject } = entity
|
||||||
|
|
||||||
// Update animations
|
// Update animations
|
||||||
|
|
@ -302,9 +360,6 @@ export class Entities {
|
||||||
playerObject.animation.update(playerObject, dt)
|
playerObject.animation.update(playerObject, dt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update armor positions
|
|
||||||
this.syncArmorPositions(entity)
|
|
||||||
|
|
||||||
// Update visibility based on distance and chunk load status
|
// Update visibility based on distance and chunk load status
|
||||||
if (botPos && entity.position) {
|
if (botPos && entity.position) {
|
||||||
const dx = entity.position.x - botPos.x
|
const dx = entity.position.x - botPos.x
|
||||||
|
|
@ -312,16 +367,37 @@ export class Entities {
|
||||||
const dz = entity.position.z - botPos.z
|
const dz = entity.position.z - botPos.z
|
||||||
const distanceSquared = dx * dx + dy * dy + dz * dz
|
const distanceSquared = dx * dx + dy * dy + dz * dz
|
||||||
|
|
||||||
// Get chunk coordinates
|
// Entity is visible if within 20 blocks OR in a finished chunk
|
||||||
const chunkX = Math.floor(entity.position.x / 16) * 16
|
entity.visible = !!(distanceSquared < VISIBLE_DISTANCE || this.worldRenderer.shouldObjectVisible(entity))
|
||||||
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.worldRenderer.finishedChunks[chunkKey])
|
|
||||||
|
|
||||||
this.maybeRenderPlayerSkin(entityId)
|
this.maybeRenderPlayerSkin(entityId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (entity.visible) {
|
||||||
|
// Update armor positions
|
||||||
|
this.syncArmorPositions(entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entityId === 'player_entity') {
|
||||||
|
entity.visible = this.worldRenderer.playerStateUtils.isThirdPerson()
|
||||||
|
|
||||||
|
if (entity.visible) {
|
||||||
|
// sync
|
||||||
|
const yOffset = this.worldRenderer.playerStateReactive.eyeHeight
|
||||||
|
const pos = this.worldRenderer.cameraObject.position.clone().add(new THREE.Vector3(0, -yOffset, 0))
|
||||||
|
entity.position.set(pos.x, pos.y, pos.z)
|
||||||
|
|
||||||
|
const rotation = this.worldRenderer.cameraShake.getBaseRotation()
|
||||||
|
entity.rotation.set(0, rotation.yaw, 0)
|
||||||
|
|
||||||
|
// Sync head rotation
|
||||||
|
entity.traverse((c) => {
|
||||||
|
if (c.name === 'head') {
|
||||||
|
c.rotation.set(-rotation.pitch, 0, 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -399,6 +475,7 @@ export class Entities {
|
||||||
}
|
}
|
||||||
|
|
||||||
getPlayerObject (entityId: string | number) {
|
getPlayerObject (entityId: string | number) {
|
||||||
|
if (this.playerEntity?.originalEntity.id === entityId) return this.playerEntity?.playerObject
|
||||||
const playerObject = this.entities[entityId]?.playerObject
|
const playerObject = this.entities[entityId]?.playerObject
|
||||||
return playerObject
|
return playerObject
|
||||||
}
|
}
|
||||||
|
|
@ -411,8 +488,13 @@ export class Entities {
|
||||||
.some(channel => channel !== 0)
|
.some(channel => channel !== 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo true/undefined doesnt reset the skin to the default one
|
||||||
// eslint-disable-next-line max-params
|
// eslint-disable-next-line max-params
|
||||||
async updatePlayerSkin (entityId: string | number, username: string | undefined, uuidCache: string | undefined, skinUrl: string | true, capeUrl: string | true | undefined = undefined) {
|
async updatePlayerSkin (entityId: string | number, username: string | undefined, uuidCache: string | undefined, skinUrl: string | true, capeUrl: string | true | undefined = undefined) {
|
||||||
|
const isCustomSkin = skinUrl !== stevePngUrl
|
||||||
|
if (isCustomSkin) {
|
||||||
|
this.loadedSkinEntityIds.add(String(entityId))
|
||||||
|
}
|
||||||
if (uuidCache) {
|
if (uuidCache) {
|
||||||
if (typeof skinUrl === 'string' || typeof capeUrl === 'string') this.uuidPerSkinUrlsCache[uuidCache] = {}
|
if (typeof skinUrl === 'string' || typeof capeUrl === 'string') this.uuidPerSkinUrlsCache[uuidCache] = {}
|
||||||
if (typeof skinUrl === 'string') this.uuidPerSkinUrlsCache[uuidCache].skinUrl = skinUrl
|
if (typeof skinUrl === 'string') this.uuidPerSkinUrlsCache[uuidCache].skinUrl = skinUrl
|
||||||
|
|
@ -467,16 +549,16 @@ export class Entities {
|
||||||
if (!playerObject) return
|
if (!playerObject) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let playerCustomSkinImage: HTMLImageElement | undefined
|
let playerCustomSkinImage: ImageBitmap | undefined
|
||||||
|
|
||||||
playerObject = this.getPlayerObject(entityId)
|
playerObject = this.getPlayerObject(entityId)
|
||||||
if (!playerObject) return
|
if (!playerObject) return
|
||||||
|
|
||||||
let skinTexture: THREE.Texture
|
let skinTexture: THREE.Texture
|
||||||
let skinCanvas: HTMLCanvasElement
|
let skinCanvas: OffscreenCanvas
|
||||||
if (skinUrl === stevePngUrl) {
|
if (skinUrl === stevePngUrl) {
|
||||||
skinTexture = await steveTexture
|
skinTexture = await steveTexture
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = createCanvas(64, 64)
|
||||||
const ctx = canvas.getContext('2d')
|
const ctx = canvas.getContext('2d')
|
||||||
if (!ctx) throw new Error('Failed to get context')
|
if (!ctx) throw new Error('Failed to get context')
|
||||||
ctx.drawImage(skinTexture.image, 0, 0)
|
ctx.drawImage(skinTexture.image, 0, 0)
|
||||||
|
|
@ -550,22 +632,70 @@ export class Entities {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
playAnimation (entityPlayerId, animation: 'walking' | 'running' | 'oneSwing' | 'idle' | 'crouch' | 'crouchWalking') {
|
debugSwingArm () {
|
||||||
const playerObject = this.getPlayerObject(entityPlayerId)
|
const playerObject = Object.values(this.entities).find(entity => entity.playerObject?.animation instanceof WalkingGeneralSwing)
|
||||||
if (!playerObject) return
|
if (!playerObject) return
|
||||||
|
(playerObject.playerObject!.animation as WalkingGeneralSwing).swingArm()
|
||||||
|
}
|
||||||
|
|
||||||
if (animation === 'oneSwing') {
|
playAnimation (entityPlayerId, animation: 'walking' | 'running' | 'oneSwing' | 'idle' | 'crouch' | 'crouchWalking') {
|
||||||
if (!(playerObject.animation instanceof WalkingGeneralSwing)) throw new Error('Expected WalkingGeneralSwing')
|
// TODO CLEANUP!
|
||||||
playerObject.animation.swingArm()
|
// Handle special player entity ID for bot entity in third person
|
||||||
|
if (entityPlayerId === 'player_entity' && this.playerEntity?.playerObject) {
|
||||||
|
const { playerObject } = this.playerEntity
|
||||||
|
if (animation === 'oneSwing') {
|
||||||
|
if (!(playerObject.animation instanceof WalkingGeneralSwing)) throw new Error('Expected WalkingGeneralSwing')
|
||||||
|
playerObject.animation.swingArm()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playerObject.animation instanceof WalkingGeneralSwing) {
|
||||||
|
playerObject.animation.switchAnimationCallback = () => {
|
||||||
|
if (!(playerObject.animation instanceof WalkingGeneralSwing)) throw new Error('Expected WalkingGeneralSwing')
|
||||||
|
playerObject.animation.isMoving = animation === 'walking' || animation === 'running' || animation === 'crouchWalking'
|
||||||
|
playerObject.animation.isRunning = animation === 'running'
|
||||||
|
playerObject.animation.isCrouched = animation === 'crouch' || animation === 'crouchWalking'
|
||||||
|
}
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playerObject.animation instanceof WalkingGeneralSwing) {
|
// Handle regular entities
|
||||||
playerObject.animation.switchAnimationCallback = () => {
|
const playerObject = this.getPlayerObject(entityPlayerId)
|
||||||
|
if (playerObject) {
|
||||||
|
if (animation === 'oneSwing') {
|
||||||
if (!(playerObject.animation instanceof WalkingGeneralSwing)) throw new Error('Expected WalkingGeneralSwing')
|
if (!(playerObject.animation instanceof WalkingGeneralSwing)) throw new Error('Expected WalkingGeneralSwing')
|
||||||
playerObject.animation.isMoving = animation === 'walking' || animation === 'running' || animation === 'crouchWalking'
|
playerObject.animation.swingArm()
|
||||||
playerObject.animation.isRunning = animation === 'running'
|
return
|
||||||
playerObject.animation.isCrouched = animation === 'crouch' || animation === 'crouchWalking'
|
}
|
||||||
|
|
||||||
|
if (playerObject.animation instanceof WalkingGeneralSwing) {
|
||||||
|
playerObject.animation.switchAnimationCallback = () => {
|
||||||
|
if (!(playerObject.animation instanceof WalkingGeneralSwing)) throw new Error('Expected WalkingGeneralSwing')
|
||||||
|
playerObject.animation.isMoving = animation === 'walking' || animation === 'running' || animation === 'crouchWalking'
|
||||||
|
playerObject.animation.isRunning = animation === 'running'
|
||||||
|
playerObject.animation.isCrouched = animation === 'crouch' || animation === 'crouchWalking'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle player entity (for third person view) - fallback for backwards compatibility
|
||||||
|
if (this.playerEntity?.playerObject) {
|
||||||
|
const { playerObject: playerEntityObject } = this.playerEntity
|
||||||
|
if (animation === 'oneSwing') {
|
||||||
|
if (!(playerEntityObject.animation instanceof WalkingGeneralSwing)) throw new Error('Expected WalkingGeneralSwing')
|
||||||
|
playerEntityObject.animation.swingArm()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playerEntityObject.animation instanceof WalkingGeneralSwing) {
|
||||||
|
playerEntityObject.animation.switchAnimationCallback = () => {
|
||||||
|
if (!(playerEntityObject.animation instanceof WalkingGeneralSwing)) throw new Error('Expected WalkingGeneralSwing')
|
||||||
|
playerEntityObject.animation.isMoving = animation === 'walking' || animation === 'running' || animation === 'crouchWalking'
|
||||||
|
playerEntityObject.animation.isRunning = animation === 'running'
|
||||||
|
playerEntityObject.animation.isCrouched = animation === 'crouch' || animation === 'crouchWalking'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -588,13 +718,13 @@ export class Entities {
|
||||||
return typeof component === 'string' ? component : component.text ?? ''
|
return typeof component === 'string' ? component : component.text ?? ''
|
||||||
}
|
}
|
||||||
|
|
||||||
getItemMesh (item, specificProps: ItemSpecificContextProperties, previousModel?: string) {
|
getItemMesh (item, specificProps: ItemSpecificContextProperties, faceCamera = false, previousModel?: string) {
|
||||||
if (!item.nbt && item.nbtData) item.nbt = item.nbtData
|
if (!item.nbt && item.nbtData) item.nbt = item.nbtData
|
||||||
const textureUv = this.worldRenderer.getItemRenderData(item, specificProps)
|
const textureUv = this.worldRenderer.getItemRenderData(item, specificProps)
|
||||||
if (previousModel && previousModel === textureUv?.modelName) return undefined
|
if (previousModel && previousModel === textureUv?.modelName) return undefined
|
||||||
|
|
||||||
if (textureUv && 'resolvedModel' in textureUv) {
|
if (textureUv && 'resolvedModel' in textureUv) {
|
||||||
const mesh = getBlockMeshFromModel(this.worldRenderer.material, textureUv.resolvedModel, textureUv.modelName, this.worldRenderer.resourcesManager.currentResources!.worldBlockProvider)
|
const mesh = getBlockMeshFromModel(this.worldRenderer.material, textureUv.resolvedModel, textureUv.modelName, this.worldRenderer.resourcesManager.currentResources.worldBlockProvider!)
|
||||||
let SCALE = 1
|
let SCALE = 1
|
||||||
if (specificProps['minecraft:display_context'] === 'ground') {
|
if (specificProps['minecraft:display_context'] === 'ground') {
|
||||||
SCALE = 0.5
|
SCALE = 0.5
|
||||||
|
|
@ -607,60 +737,41 @@ export class Entities {
|
||||||
return {
|
return {
|
||||||
mesh: outerGroup,
|
mesh: outerGroup,
|
||||||
isBlock: true,
|
isBlock: true,
|
||||||
itemsTexture: null,
|
|
||||||
itemsTextureFlipped: null,
|
|
||||||
modelName: textureUv.modelName,
|
modelName: textureUv.modelName,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Render proper model (especially for blocks) instead of flat texture
|
// Render proper 3D model for items
|
||||||
if (textureUv) {
|
if (textureUv) {
|
||||||
const textureThree = textureUv.renderInfo?.texture === 'blocks' ? this.worldRenderer.material.map! : this.worldRenderer.itemsTexture
|
const textureThree = textureUv.renderInfo?.texture === 'blocks' ? this.worldRenderer.material.map! : this.worldRenderer.itemsTexture
|
||||||
// todo use geometry buffer uv instead!
|
|
||||||
const { u, v, su, sv } = textureUv
|
const { u, v, su, sv } = textureUv
|
||||||
const size = undefined
|
const sizeX = su ?? 1 // su is actually width
|
||||||
const itemsTexture = textureThree.clone()
|
const sizeY = sv ?? 1 // sv is actually height
|
||||||
itemsTexture.flipY = true
|
|
||||||
const sizeY = (sv ?? size)!
|
// Use the new unified item mesh function
|
||||||
const sizeX = (su ?? size)!
|
const result = createItemMesh(textureThree, {
|
||||||
itemsTexture.offset.set(u, 1 - v - sizeY)
|
u,
|
||||||
itemsTexture.repeat.set(sizeX, sizeY)
|
v,
|
||||||
itemsTexture.needsUpdate = true
|
sizeX,
|
||||||
itemsTexture.magFilter = THREE.NearestFilter
|
sizeY
|
||||||
itemsTexture.minFilter = THREE.NearestFilter
|
}, {
|
||||||
const itemsTextureFlipped = itemsTexture.clone()
|
faceCamera,
|
||||||
itemsTextureFlipped.repeat.x *= -1
|
use3D: !faceCamera, // Only use 3D for non-camera-facing items
|
||||||
itemsTextureFlipped.needsUpdate = true
|
|
||||||
itemsTextureFlipped.offset.set(u + (sizeX), 1 - v - sizeY)
|
|
||||||
const material = new THREE.MeshStandardMaterial({
|
|
||||||
map: itemsTexture,
|
|
||||||
transparent: true,
|
|
||||||
alphaTest: 0.1,
|
|
||||||
})
|
})
|
||||||
const materialFlipped = new THREE.MeshStandardMaterial({
|
|
||||||
map: itemsTextureFlipped,
|
|
||||||
transparent: true,
|
|
||||||
alphaTest: 0.1,
|
|
||||||
})
|
|
||||||
const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 0), [
|
|
||||||
// top left and right bottom are black box materials others are transparent
|
|
||||||
new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), new THREE.MeshBasicMaterial({ color: 0x00_00_00 }),
|
|
||||||
new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), new THREE.MeshBasicMaterial({ color: 0x00_00_00 }),
|
|
||||||
material, materialFlipped,
|
|
||||||
])
|
|
||||||
let SCALE = 1
|
let SCALE = 1
|
||||||
if (specificProps['minecraft:display_context'] === 'ground') {
|
if (specificProps['minecraft:display_context'] === 'ground') {
|
||||||
SCALE = 0.5
|
SCALE = 0.5
|
||||||
} else if (specificProps['minecraft:display_context'] === 'thirdperson') {
|
} else if (specificProps['minecraft:display_context'] === 'thirdperson') {
|
||||||
SCALE = 6
|
SCALE = 6
|
||||||
}
|
}
|
||||||
mesh.scale.set(SCALE, SCALE, SCALE)
|
result.mesh.scale.set(SCALE, SCALE, SCALE)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mesh,
|
mesh: result.mesh,
|
||||||
isBlock: false,
|
isBlock: false,
|
||||||
itemsTexture,
|
|
||||||
itemsTextureFlipped,
|
|
||||||
modelName: textureUv.modelName,
|
modelName: textureUv.modelName,
|
||||||
|
cleanup: result.cleanup
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -675,9 +786,7 @@ export class Entities {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update (entity: import('prismarine-entity').Entity & { delete?; pos, name }, overrides) {
|
update (entity: SceneEntity['originalEntity'], overrides) {
|
||||||
const justAdded = !this.entities[entity.id]
|
|
||||||
|
|
||||||
const isPlayerModel = entity.name === 'player'
|
const isPlayerModel = entity.name === 'player'
|
||||||
if (entity.name === 'zombie_villager' || entity.name === 'husk') {
|
if (entity.name === 'zombie_villager' || entity.name === 'husk') {
|
||||||
overrides.texture = `textures/1.16.4/entity/${entity.name === 'zombie_villager' ? 'zombie_villager/zombie_villager.png' : `zombie/${entity.name}.png`}`
|
overrides.texture = `textures/1.16.4/entity/${entity.name === 'zombie_villager' ? 'zombie_villager/zombie_villager.png' : `zombie/${entity.name}.png`}`
|
||||||
|
|
@ -688,6 +797,7 @@ export class Entities {
|
||||||
}
|
}
|
||||||
// this can be undefined in case where packet entity_destroy was sent twice (so it was already deleted)
|
// this can be undefined in case where packet entity_destroy was sent twice (so it was already deleted)
|
||||||
let e = this.entities[entity.id]
|
let e = this.entities[entity.id]
|
||||||
|
const justAdded = !e
|
||||||
|
|
||||||
if (entity.delete) {
|
if (entity.delete) {
|
||||||
if (!e) return
|
if (!e) return
|
||||||
|
|
@ -703,24 +813,27 @@ export class Entities {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let mesh
|
let mesh: THREE.Object3D | undefined
|
||||||
if (e === undefined) {
|
if (e === undefined) {
|
||||||
const group = new THREE.Group()
|
const group = new THREE.Group() as unknown as SceneEntity
|
||||||
if (entity.name === 'item' || entity.name === 'tnt' || entity.name === 'falling_block') {
|
group.originalEntity = entity
|
||||||
const item = entity.name === 'tnt'
|
if (entity.name === 'item' || entity.name === 'tnt' || entity.name === 'falling_block' || entity.name === 'snowball'
|
||||||
? { name: 'tnt' }
|
|| entity.name === 'egg' || entity.name === 'ender_pearl' || entity.name === 'experience_bottle'
|
||||||
|
|| entity.name === 'splash_potion' || entity.name === 'lingering_potion') {
|
||||||
|
const item = entity.name === 'tnt' || entity.type === 'projectile'
|
||||||
|
? { name: entity.name }
|
||||||
: entity.name === 'falling_block'
|
: entity.name === 'falling_block'
|
||||||
? { blockState: entity['objectData'] }
|
? { blockState: entity['objectData'] }
|
||||||
: entity.metadata?.find((m: any) => typeof m === 'object' && m?.itemCount)
|
: entity.metadata?.find((m: any) => typeof m === 'object' && m?.itemCount)
|
||||||
if (item) {
|
if (item) {
|
||||||
const object = this.getItemMesh(item, {
|
const object = this.getItemMesh(item, {
|
||||||
'minecraft:display_context': 'ground',
|
'minecraft:display_context': 'ground',
|
||||||
})
|
}, entity.type === 'projectile')
|
||||||
if (object) {
|
if (object) {
|
||||||
mesh = object.mesh
|
mesh = object.mesh
|
||||||
if (entity.name === 'item') {
|
if (entity.name === 'item' || entity.type === 'projectile') {
|
||||||
mesh.scale.set(0.5, 0.5, 0.5)
|
mesh.scale.set(0.5, 0.5, 0.5)
|
||||||
mesh.position.set(0, 0.2, 0)
|
mesh.position.set(0, entity.name === 'item' ? 0.2 : 0.1, 0)
|
||||||
} else {
|
} else {
|
||||||
mesh.scale.set(2, 2, 2)
|
mesh.scale.set(2, 2, 2)
|
||||||
mesh.position.set(0, 0.5, 0)
|
mesh.position.set(0, 0.5, 0)
|
||||||
|
|
@ -728,11 +841,11 @@ export class Entities {
|
||||||
// set faces
|
// set faces
|
||||||
// mesh.position.set(targetPos.x + 0.5 + 2, targetPos.y + 0.5, targetPos.z + 0.5)
|
// mesh.position.set(targetPos.x + 0.5 + 2, targetPos.y + 0.5, targetPos.z + 0.5)
|
||||||
// viewer.scene.add(mesh)
|
// viewer.scene.add(mesh)
|
||||||
const clock = new THREE.Clock()
|
|
||||||
if (entity.name === 'item') {
|
if (entity.name === 'item') {
|
||||||
|
const clock = new THREE.Clock()
|
||||||
mesh.onBeforeRender = () => {
|
mesh.onBeforeRender = () => {
|
||||||
const delta = clock.getDelta()
|
const delta = clock.getDelta()
|
||||||
mesh.rotation.y += delta
|
mesh!.rotation.y += delta
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -756,59 +869,35 @@ export class Entities {
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
//@ts-expect-error
|
|
||||||
group.additionalCleanup = () => {
|
group.additionalCleanup = () => {
|
||||||
// important: avoid texture memory leak and gpu slowdown
|
// important: avoid texture memory leak and gpu slowdown
|
||||||
object.itemsTexture?.dispose()
|
if (object.cleanup) {
|
||||||
object.itemsTextureFlipped?.dispose()
|
object.cleanup()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (isPlayerModel) {
|
} else if (isPlayerModel) {
|
||||||
// CREATE NEW PLAYER ENTITY
|
|
||||||
const wrapper = new THREE.Group()
|
const wrapper = new THREE.Group()
|
||||||
const playerObject = new PlayerObject() as PlayerObjectType
|
const playerObject = this.setupPlayerObject(entity, wrapper, overrides)
|
||||||
playerObject.realPlayerUuid = entity.uuid ?? ''
|
group.playerObject = playerObject
|
||||||
playerObject.realUsername = entity.username ?? ''
|
mesh = wrapper
|
||||||
playerObject.position.set(0, 16, 0)
|
|
||||||
|
|
||||||
// fix issues with starfield
|
|
||||||
playerObject.traverse((obj) => {
|
|
||||||
if (obj instanceof THREE.Mesh && obj.material instanceof THREE.MeshStandardMaterial) {
|
|
||||||
obj.material.transparent = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
//@ts-expect-error
|
|
||||||
wrapper.add(playerObject)
|
|
||||||
const scale = 1 / 16
|
|
||||||
wrapper.scale.set(scale, scale, scale)
|
|
||||||
|
|
||||||
if (entity.username) {
|
if (entity.username) {
|
||||||
// todo proper colors
|
const nametag = addNametag(entity, { fontFamily: 'mojangles' }, wrapper, this.worldRenderer.version)
|
||||||
const nameTag = new NameTagObject(fromFormattedString(entity.username).text, {
|
if (nametag) {
|
||||||
font: `48px ${this.entitiesOptions.fontFamily}`,
|
nametag.position.y = playerObject.position.y + playerObject.scale.y * 16 + 3
|
||||||
})
|
nametag.scale.multiplyScalar(12)
|
||||||
nameTag.position.y = playerObject.position.y + playerObject.scale.y * 16 + 3
|
}
|
||||||
nameTag.renderOrder = 1000
|
|
||||||
|
|
||||||
//@ts-expect-error
|
|
||||||
wrapper.add(nameTag)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//@ts-expect-error
|
|
||||||
group.playerObject = playerObject
|
|
||||||
wrapper.rotation.set(0, Math.PI, 0)
|
|
||||||
mesh = wrapper
|
|
||||||
playerObject.animation = new WalkingGeneralSwing()
|
|
||||||
//@ts-expect-error
|
|
||||||
playerObject.animation.isMoving = false
|
|
||||||
} else {
|
} else {
|
||||||
mesh = getEntityMesh(entity, this.worldRenderer, this.entitiesOptions, overrides)
|
mesh = getEntityMesh(entity, this.worldRenderer, this.entitiesOptions, { ...overrides, customModel: entity['customModel'] })
|
||||||
}
|
}
|
||||||
if (!mesh) return
|
if (!mesh) return
|
||||||
mesh.name = 'mesh'
|
mesh.name = 'mesh'
|
||||||
// set initial position so there are no weird jumps update after
|
// set initial position so there are no weird jumps update after
|
||||||
group.position.set(entity.pos.x, entity.pos.y, entity.pos.z)
|
const pos = entity.pos ?? entity.position
|
||||||
|
group.position.set(pos.x, pos.y, pos.z)
|
||||||
|
|
||||||
// todo use width and height instead
|
// todo use width and height instead
|
||||||
const boxHelper = new THREE.BoxHelper(
|
const boxHelper = new THREE.BoxHelper(
|
||||||
|
|
@ -840,23 +929,13 @@ export class Entities {
|
||||||
mesh = e.children.find(c => c.name === 'mesh')
|
mesh = e.children.find(c => c.name === 'mesh')
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if entity has armor
|
// Update equipment
|
||||||
if (entity.equipment) {
|
this.updateEntityEquipment(e, entity)
|
||||||
const isPlayer = entity.type === 'player'
|
|
||||||
this.addItemModel(e, isPlayer ? 'right' : 'left', entity.equipment[0], isPlayer)
|
|
||||||
this.addItemModel(e, isPlayer ? 'left' : 'right', entity.equipment[1], isPlayer)
|
|
||||||
addArmorModel(this.worldRenderer, e, 'feet', entity.equipment[2])
|
|
||||||
addArmorModel(this.worldRenderer, e, 'legs', entity.equipment[3], 2)
|
|
||||||
addArmorModel(this.worldRenderer, e, 'chest', entity.equipment[4])
|
|
||||||
addArmorModel(this.worldRenderer, e, 'head', entity.equipment[5])
|
|
||||||
}
|
|
||||||
|
|
||||||
const meta = getGeneralEntitiesMetadata(entity)
|
const meta = getGeneralEntitiesMetadata(entity)
|
||||||
|
|
||||||
//@ts-expect-error
|
const isInvisible = ((entity.metadata?.[0] ?? 0) as unknown as number) & 0x20 || (this.worldRenderer.playerStateReactive.cameraSpectatingEntity === entity.id && this.worldRenderer.playerStateUtils.isSpectator())
|
||||||
// set visibility
|
for (const child of mesh!.children ?? []) {
|
||||||
const isInvisible = entity.metadata?.[0] & 0x20
|
|
||||||
for (const child of mesh.children ?? []) {
|
|
||||||
if (child.name !== 'nametag') {
|
if (child.name !== 'nametag') {
|
||||||
child.visible = !isInvisible
|
child.visible = !isInvisible
|
||||||
}
|
}
|
||||||
|
|
@ -871,21 +950,22 @@ export class Entities {
|
||||||
// entity specific meta
|
// entity specific meta
|
||||||
const textDisplayMeta = getSpecificEntityMetadata('text_display', entity)
|
const textDisplayMeta = getSpecificEntityMetadata('text_display', entity)
|
||||||
const displayTextRaw = textDisplayMeta?.text || meta.custom_name_visible && meta.custom_name
|
const displayTextRaw = textDisplayMeta?.text || meta.custom_name_visible && meta.custom_name
|
||||||
const displayText = this.parseEntityLabel(displayTextRaw)
|
if (entity.name !== 'player' && displayTextRaw) {
|
||||||
if (entity.name !== 'player' && displayText) {
|
|
||||||
const nameTagFixed = textDisplayMeta && (textDisplayMeta.billboard_render_constraints === 'fixed' || !textDisplayMeta.billboard_render_constraints)
|
const nameTagFixed = textDisplayMeta && (textDisplayMeta.billboard_render_constraints === 'fixed' || !textDisplayMeta.billboard_render_constraints)
|
||||||
const nameTagBackgroundColor = textDisplayMeta && toRgba(textDisplayMeta.background_color)
|
const nameTagBackgroundColor = (textDisplayMeta && (parseInt(textDisplayMeta.style_flags, 10) & 0x04) === 0) ? toRgba(textDisplayMeta.background_color) : undefined
|
||||||
let nameTagTextOpacity: any
|
let nameTagTextOpacity: any
|
||||||
if (textDisplayMeta?.text_opacity) {
|
if (textDisplayMeta?.text_opacity) {
|
||||||
const rawOpacity = parseInt(textDisplayMeta?.text_opacity, 10)
|
const rawOpacity = parseInt(textDisplayMeta?.text_opacity, 10)
|
||||||
nameTagTextOpacity = rawOpacity > 0 ? rawOpacity : 256 - rawOpacity
|
nameTagTextOpacity = rawOpacity > 0 ? rawOpacity : 256 - rawOpacity
|
||||||
}
|
}
|
||||||
addNametag(
|
addNametag(
|
||||||
{ ...entity, username: displayText, nameTagBackgroundColor, nameTagTextOpacity, nameTagFixed,
|
{ ...entity, username: typeof displayTextRaw === 'string' ? mojangson.simplify(mojangson.parse(displayTextRaw)) : nbt.simplify(displayTextRaw),
|
||||||
|
nameTagBackgroundColor, nameTagTextOpacity, nameTagFixed,
|
||||||
nameTagScale: textDisplayMeta?.scale, nameTagTranslation: textDisplayMeta && (textDisplayMeta.translation || new THREE.Vector3(0, 0, 0)),
|
nameTagScale: textDisplayMeta?.scale, nameTagTranslation: textDisplayMeta && (textDisplayMeta.translation || new THREE.Vector3(0, 0, 0)),
|
||||||
nameTagRotationLeft: toQuaternion(textDisplayMeta?.left_rotation), nameTagRotationRight: toQuaternion(textDisplayMeta?.right_rotation) },
|
nameTagRotationLeft: toQuaternion(textDisplayMeta?.left_rotation), nameTagRotationRight: toQuaternion(textDisplayMeta?.right_rotation) },
|
||||||
this.entitiesOptions,
|
this.entitiesOptions,
|
||||||
mesh
|
mesh,
|
||||||
|
this.worldRenderer.version
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -895,8 +975,8 @@ export class Entities {
|
||||||
const hasArms = (parseInt(armorStandMeta.client_flags, 10) & 0x04) !== 0
|
const hasArms = (parseInt(armorStandMeta.client_flags, 10) & 0x04) !== 0
|
||||||
const hasBasePlate = (parseInt(armorStandMeta.client_flags, 10) & 0x08) === 0
|
const hasBasePlate = (parseInt(armorStandMeta.client_flags, 10) & 0x08) === 0
|
||||||
const isMarker = (parseInt(armorStandMeta.client_flags, 10) & 0x10) !== 0
|
const isMarker = (parseInt(armorStandMeta.client_flags, 10) & 0x10) !== 0
|
||||||
mesh.castShadow = !isMarker
|
mesh!.castShadow = !isMarker
|
||||||
mesh.receiveShadow = !isMarker
|
mesh!.receiveShadow = !isMarker
|
||||||
if (isSmall) {
|
if (isSmall) {
|
||||||
e.scale.set(0.5, 0.5, 0.5)
|
e.scale.set(0.5, 0.5, 0.5)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -965,7 +1045,9 @@ export class Entities {
|
||||||
// TODO: fix type
|
// TODO: fix type
|
||||||
// todo! fix errors in mc-data (no entities data prior 1.18.2)
|
// todo! fix errors in mc-data (no entities data prior 1.18.2)
|
||||||
const item = (itemFrameMeta?.item ?? entity.metadata?.[8]) as any as { itemId, blockId, components, nbtData: { value: { map: { value: number } } } }
|
const item = (itemFrameMeta?.item ?? entity.metadata?.[8]) as any as { itemId, blockId, components, nbtData: { value: { map: { value: number } } } }
|
||||||
mesh.scale.set(1, 1, 1)
|
mesh!.scale.set(1, 1, 1)
|
||||||
|
mesh!.position.set(0, 0, -0.5)
|
||||||
|
|
||||||
e.rotation.x = -entity.pitch
|
e.rotation.x = -entity.pitch
|
||||||
e.children.find(c => {
|
e.children.find(c => {
|
||||||
if (c.name.startsWith('map_')) {
|
if (c.name.startsWith('map_')) {
|
||||||
|
|
@ -982,25 +1064,33 @@ export class Entities {
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
})?.removeFromParent()
|
})?.removeFromParent()
|
||||||
|
|
||||||
if (item && (item.itemId ?? item.blockId ?? 0) !== 0) {
|
if (item && (item.itemId ?? item.blockId ?? 0) !== 0) {
|
||||||
|
// Get rotation from metadata, default to 0 if not present
|
||||||
|
// Rotation is stored in 45° increments (0-7) for items, 90° increments (0-3) for maps
|
||||||
const rotation = (itemFrameMeta.rotation as any as number) ?? 0
|
const rotation = (itemFrameMeta.rotation as any as number) ?? 0
|
||||||
const mapNumber = item.nbtData?.value?.map?.value ?? item.components?.find(x => x.type === 'map_id')?.data
|
const mapNumber = item.nbtData?.value?.map?.value ?? item.components?.find(x => x.type === 'map_id')?.data
|
||||||
if (mapNumber) {
|
if (mapNumber) {
|
||||||
// TODO: Use proper larger item frame model when a map exists
|
// TODO: Use proper larger item frame model when a map exists
|
||||||
mesh.scale.set(16 / 12, 16 / 12, 1)
|
mesh!.scale.set(16 / 12, 16 / 12, 1)
|
||||||
|
// Handle map rotation (4 possibilities, 90° increments)
|
||||||
this.addMapModel(e, mapNumber, rotation)
|
this.addMapModel(e, mapNumber, rotation)
|
||||||
} else {
|
} else {
|
||||||
|
// Handle regular item rotation (8 possibilities, 45° increments)
|
||||||
const itemMesh = this.getItemMesh(item, {
|
const itemMesh = this.getItemMesh(item, {
|
||||||
'minecraft:display_context': 'fixed',
|
'minecraft:display_context': 'fixed',
|
||||||
})
|
})
|
||||||
if (itemMesh) {
|
if (itemMesh) {
|
||||||
itemMesh.mesh.position.set(0, 0, 0.43)
|
itemMesh.mesh.position.set(0, 0, -0.05)
|
||||||
|
// itemMesh.mesh.position.set(0, 0, 0.43)
|
||||||
if (itemMesh.isBlock) {
|
if (itemMesh.isBlock) {
|
||||||
itemMesh.mesh.scale.set(0.25, 0.25, 0.25)
|
itemMesh.mesh.scale.set(0.25, 0.25, 0.25)
|
||||||
} else {
|
} else {
|
||||||
itemMesh.mesh.scale.set(0.5, 0.5, 0.5)
|
itemMesh.mesh.scale.set(0.5, 0.5, 0.5)
|
||||||
}
|
}
|
||||||
|
// Rotate 180° around Y axis first
|
||||||
itemMesh.mesh.rotateY(Math.PI)
|
itemMesh.mesh.rotateY(Math.PI)
|
||||||
|
// Then apply the 45° increment rotation
|
||||||
itemMesh.mesh.rotateZ(-rotation * Math.PI / 4)
|
itemMesh.mesh.rotateZ(-rotation * Math.PI / 4)
|
||||||
itemMesh.mesh.name = 'item'
|
itemMesh.mesh.name = 'item'
|
||||||
e.add(itemMesh.mesh)
|
e.add(itemMesh.mesh)
|
||||||
|
|
@ -1009,17 +1099,11 @@ export class Entities {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entity.username) {
|
if (entity.username !== undefined) {
|
||||||
e.username = entity.username
|
e.username = entity.username
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entity.type === 'player' && entity.equipment && e.playerObject) {
|
this.updateNameTagVisibility(e)
|
||||||
const { playerObject } = e
|
|
||||||
playerObject.backEquipment = entity.equipment.some((item) => item?.name === 'elytra') ? 'elytra' : 'cape'
|
|
||||||
if (playerObject.cape.map === null) {
|
|
||||||
playerObject.cape.visible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateEntityPosition(entity, justAdded, overrides)
|
this.updateEntityPosition(entity, justAdded, overrides)
|
||||||
}
|
}
|
||||||
|
|
@ -1050,17 +1134,20 @@ export class Entities {
|
||||||
|
|
||||||
loadedSkinEntityIds = new Set<string>()
|
loadedSkinEntityIds = new Set<string>()
|
||||||
maybeRenderPlayerSkin (entityId: string) {
|
maybeRenderPlayerSkin (entityId: string) {
|
||||||
const mesh = this.entities[entityId]
|
let mesh = this.entities[entityId]
|
||||||
|
if (entityId === 'player_entity') {
|
||||||
|
mesh = this.playerEntity!
|
||||||
|
entityId = this.playerEntity?.originalEntity.id as any
|
||||||
|
}
|
||||||
if (!mesh) return
|
if (!mesh) return
|
||||||
if (!mesh.playerObject) return
|
if (!mesh.playerObject) return
|
||||||
if (!mesh.visible) return
|
if (!mesh.visible) return
|
||||||
|
|
||||||
const MAX_DISTANCE_SKIN_LOAD = 128
|
const MAX_DISTANCE_SKIN_LOAD = 128
|
||||||
const cameraPos = this.worldRenderer.camera.position
|
const cameraPos = this.worldRenderer.cameraObject.position
|
||||||
const distance = mesh.position.distanceTo(cameraPos)
|
const distance = mesh.position.distanceTo(cameraPos)
|
||||||
if (distance < MAX_DISTANCE_SKIN_LOAD && distance < (this.worldRenderer.viewDistance * 16)) {
|
if (distance < MAX_DISTANCE_SKIN_LOAD && distance < (this.worldRenderer.viewDistance * 16)) {
|
||||||
if (this.loadedSkinEntityIds.has(entityId)) return
|
if (this.loadedSkinEntityIds.has(String(entityId))) return
|
||||||
this.loadedSkinEntityIds.add(entityId)
|
|
||||||
void this.updatePlayerSkin(entityId, mesh.playerObject.realUsername, mesh.playerObject.realPlayerUuid, true, true)
|
void this.updatePlayerSkin(entityId, mesh.playerObject.realUsername, mesh.playerObject.realPlayerUuid, true, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1085,6 +1172,20 @@ export class Entities {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateNameTagVisibility (entity: SceneEntity) {
|
||||||
|
const playerTeam = this.worldRenderer.playerStateReactive.team
|
||||||
|
const entityTeam = entity.originalEntity.team
|
||||||
|
const nameTagVisibility = entityTeam?.nameTagVisibility || 'always'
|
||||||
|
const showNameTag = nameTagVisibility === 'always' ||
|
||||||
|
(nameTagVisibility === 'hideForOwnTeam' && entityTeam?.team !== playerTeam?.team) ||
|
||||||
|
(nameTagVisibility === 'hideForOtherTeams' && (entityTeam?.team === playerTeam?.team || playerTeam === undefined))
|
||||||
|
entity.traverse(c => {
|
||||||
|
if (c.name === 'nametag') {
|
||||||
|
c.visible = showNameTag
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
addMapModel (entityMesh: THREE.Object3D, mapNumber: number, rotation: number) {
|
addMapModel (entityMesh: THREE.Object3D, mapNumber: number, rotation: number) {
|
||||||
const imageData = this.cachedMapsImages?.[mapNumber]
|
const imageData = this.cachedMapsImages?.[mapNumber]
|
||||||
let texture: THREE.Texture | null = null
|
let texture: THREE.Texture | null = null
|
||||||
|
|
@ -1115,6 +1216,7 @@ export class Entities {
|
||||||
} else {
|
} else {
|
||||||
mapMesh.position.set(0, 0, 0.437)
|
mapMesh.position.set(0, 0, 0.437)
|
||||||
}
|
}
|
||||||
|
// Apply 90° increment rotation for maps (0-3)
|
||||||
mapMesh.rotateZ(Math.PI * 2 - rotation * Math.PI / 2)
|
mapMesh.rotateZ(Math.PI * 2 - rotation * Math.PI / 2)
|
||||||
mapMesh.name = `map_${mapNumber}`
|
mapMesh.name = `map_${mapNumber}`
|
||||||
|
|
||||||
|
|
@ -1160,8 +1262,9 @@ export class Entities {
|
||||||
const group = new THREE.Object3D()
|
const group = new THREE.Object3D()
|
||||||
group['additionalCleanup'] = () => {
|
group['additionalCleanup'] = () => {
|
||||||
// important: avoid texture memory leak and gpu slowdown
|
// important: avoid texture memory leak and gpu slowdown
|
||||||
itemObject.itemsTexture?.dispose()
|
if (itemObject.cleanup) {
|
||||||
itemObject.itemsTextureFlipped?.dispose()
|
itemObject.cleanup()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const itemMesh = itemObject.mesh
|
const itemMesh = itemObject.mesh
|
||||||
group.rotation.z = -Math.PI / 16
|
group.rotation.z = -Math.PI / 16
|
||||||
|
|
@ -1208,13 +1311,63 @@ export class Entities {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
raycastScene () {
|
raycastSceneDebug () {
|
||||||
// return any object from scene. raycast from camera
|
// return any object from scene. raycast from camera
|
||||||
const raycaster = new THREE.Raycaster()
|
const raycaster = new THREE.Raycaster()
|
||||||
raycaster.setFromCamera(new THREE.Vector2(0, 0), this.worldRenderer.camera)
|
raycaster.setFromCamera(new THREE.Vector2(0, 0), this.worldRenderer.camera)
|
||||||
const intersects = raycaster.intersectObjects(this.worldRenderer.scene.children)
|
const intersects = raycaster.intersectObjects(this.worldRenderer.scene.children)
|
||||||
return intersects[0]?.object
|
return intersects[0]?.object
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setupPlayerObject (entity: SceneEntity['originalEntity'], wrapper: THREE.Group, overrides: { texture?: string }): PlayerObjectType {
|
||||||
|
const playerObject = new PlayerObject() as PlayerObjectType
|
||||||
|
playerObject.realPlayerUuid = entity.uuid ?? ''
|
||||||
|
playerObject.realUsername = entity.username ?? ''
|
||||||
|
playerObject.position.set(0, 16, 0)
|
||||||
|
|
||||||
|
// fix issues with starfield
|
||||||
|
playerObject.traverse((obj) => {
|
||||||
|
if (obj instanceof THREE.Mesh && obj.material instanceof THREE.MeshStandardMaterial) {
|
||||||
|
obj.material.transparent = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
wrapper.add(playerObject as any)
|
||||||
|
const scale = 1 / 16
|
||||||
|
wrapper.scale.set(scale, scale, scale)
|
||||||
|
wrapper.rotation.set(0, Math.PI, 0)
|
||||||
|
|
||||||
|
// Set up animation
|
||||||
|
playerObject.animation = new WalkingGeneralSwing()
|
||||||
|
//@ts-expect-error
|
||||||
|
playerObject.animation.isMoving = false
|
||||||
|
|
||||||
|
return playerObject
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateEntityEquipment (entityMesh: SceneEntity, entity: SceneEntity['originalEntity']) {
|
||||||
|
if (!entityMesh || !entity.equipment) return
|
||||||
|
|
||||||
|
const isPlayer = entity.type === 'player'
|
||||||
|
this.addItemModel(entityMesh, isPlayer ? 'right' : 'left', entity.equipment[0], isPlayer)
|
||||||
|
this.addItemModel(entityMesh, isPlayer ? 'left' : 'right', entity.equipment[1], isPlayer)
|
||||||
|
addArmorModel(this.worldRenderer, entityMesh, 'feet', entity.equipment[2])
|
||||||
|
addArmorModel(this.worldRenderer, entityMesh, 'legs', entity.equipment[3], 2)
|
||||||
|
addArmorModel(this.worldRenderer, entityMesh, 'chest', entity.equipment[4])
|
||||||
|
addArmorModel(this.worldRenderer, entityMesh, 'head', entity.equipment[5])
|
||||||
|
|
||||||
|
// Update player-specific equipment
|
||||||
|
if (isPlayer && entityMesh.playerObject) {
|
||||||
|
const { playerObject } = entityMesh
|
||||||
|
playerObject.backEquipment = entity.equipment.some((item) => item?.name === 'elytra') ? 'elytra' : 'cape'
|
||||||
|
if (playerObject.backEquipment === 'elytra') {
|
||||||
|
void this.loadAndApplyCape(entity.id, elytraTexture)
|
||||||
|
}
|
||||||
|
if (playerObject.cape.map === null) {
|
||||||
|
playerObject.cape.visible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGeneralEntitiesMetadata (entity: { name; metadata }): Partial<UnionToIntersection<EntityMetadataVersions[keyof EntityMetadataVersions]>> {
|
function getGeneralEntitiesMetadata (entity: { name; metadata }): Partial<UnionToIntersection<EntityMetadataVersions[keyof EntityMetadataVersions]>> {
|
||||||
|
|
@ -1255,6 +1408,11 @@ function addArmorModel (worldRenderer: WorldRendererThree, entityMesh: THREE.Obj
|
||||||
if (textureData) {
|
if (textureData) {
|
||||||
const decodedData = JSON.parse(Buffer.from(textureData, 'base64').toString())
|
const decodedData = JSON.parse(Buffer.from(textureData, 'base64').toString())
|
||||||
texturePath = decodedData.textures?.SKIN?.url
|
texturePath = decodedData.textures?.SKIN?.url
|
||||||
|
const { skinTexturesProxy } = this.worldRenderer.worldRendererConfig
|
||||||
|
if (skinTexturesProxy) {
|
||||||
|
texturePath = texturePath?.replace('http://textures.minecraft.net/', skinTexturesProxy)
|
||||||
|
.replace('https://textures.minecraft.net/', skinTexturesProxy)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error decoding player head texture:', err)
|
console.error('Error decoding player head texture:', err)
|
||||||
|
|
@ -1267,7 +1425,7 @@ function addArmorModel (worldRenderer: WorldRendererThree, entityMesh: THREE.Obj
|
||||||
if (!texturePath) {
|
if (!texturePath) {
|
||||||
// TODO: Support mirroring on certain parts of the model
|
// TODO: Support mirroring on certain parts of the model
|
||||||
const armorTextureName = `${armorMaterial}_layer_${layer}${overlay ? '_overlay' : ''}`
|
const armorTextureName = `${armorMaterial}_layer_${layer}${overlay ? '_overlay' : ''}`
|
||||||
texturePath = worldRenderer.resourcesManager.currentResources!.customTextures.armor?.textures[armorTextureName]?.src ?? armorTextures[armorTextureName]
|
texturePath = worldRenderer.resourcesManager.currentResources.customTextures.armor?.textures[armorTextureName]?.src ?? armorTextures[armorTextureName]
|
||||||
}
|
}
|
||||||
if (!texturePath || !armorModel[slotType]) {
|
if (!texturePath || !armorModel[slotType]) {
|
||||||
removeArmorModel(entityMesh, slotType)
|
removeArmorModel(entityMesh, slotType)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import ocelotPng from '../../../../node_modules/mc-assets/dist/other-textures/la
|
||||||
import arrowTexture from '../../../../node_modules/mc-assets/dist/other-textures/1.21.2/entity/projectiles/arrow.png'
|
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 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 tippedArrowTexture from '../../../../node_modules/mc-assets/dist/other-textures/1.21.2/entity/projectiles/tipped_arrow.png'
|
||||||
import { loadTexture } from '../../lib/utils'
|
import { loadTexture } from '../threeJsUtils'
|
||||||
import { WorldRendererThree } from '../worldrendererThree'
|
import { WorldRendererThree } from '../worldrendererThree'
|
||||||
import entities from './entities.json'
|
import entities from './entities.json'
|
||||||
import { externalModels } from './objModels'
|
import { externalModels } from './objModels'
|
||||||
|
|
@ -238,10 +238,11 @@ export function getMesh (
|
||||||
if (useBlockTexture) {
|
if (useBlockTexture) {
|
||||||
if (!worldRenderer) throw new Error('worldRenderer is required for block textures')
|
if (!worldRenderer) throw new Error('worldRenderer is required for block textures')
|
||||||
const blockName = texture.slice(6)
|
const blockName = texture.slice(6)
|
||||||
const textureInfo = worldRenderer.resourcesManager.currentResources!.blocksAtlasParser.getTextureInfo(blockName)
|
const textureInfo = worldRenderer.resourcesManager.currentResources.blocksAtlasJson.textures[blockName]
|
||||||
if (textureInfo) {
|
if (textureInfo) {
|
||||||
textureWidth = blocksTexture?.image.width ?? textureWidth
|
textureWidth = blocksTexture?.image.width ?? textureWidth
|
||||||
textureHeight = blocksTexture?.image.height ?? textureHeight
|
textureHeight = blocksTexture?.image.height ?? textureHeight
|
||||||
|
// todo support su/sv
|
||||||
textureOffset = [textureInfo.u, textureInfo.v]
|
textureOffset = [textureInfo.u, textureInfo.v]
|
||||||
} else {
|
} else {
|
||||||
console.error(`Unknown block ${blockName}`)
|
console.error(`Unknown block ${blockName}`)
|
||||||
|
|
@ -546,4 +547,4 @@ export class EntityMesh {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.EntityMesh = EntityMesh
|
globalThis.EntityMesh = EntityMesh
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { default as netheriteLayer1 } from 'mc-assets/dist/other-textures/latest
|
||||||
import { default as netheriteLayer2 } from 'mc-assets/dist/other-textures/latest/models/armor/netherite_layer_2.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'
|
import { default as turtleLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/turtle_layer_1.png'
|
||||||
|
|
||||||
|
export { default as elytraTexture } from 'mc-assets/dist/other-textures/latest/entity/elytra.png'
|
||||||
export { default as armorModel } from './armorModels.json'
|
export { default as armorModel } from './armorModels.json'
|
||||||
|
|
||||||
export const armorTextures = {
|
export const armorTextures = {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { ProgressReporter } from '../../../src/core/progressReporter'
|
||||||
import { showNotification } from '../../../src/react/NotificationProvider'
|
import { showNotification } from '../../../src/react/NotificationProvider'
|
||||||
import { displayEntitiesDebugList } from '../../playground/allEntitiesDebug'
|
import { displayEntitiesDebugList } from '../../playground/allEntitiesDebug'
|
||||||
import supportedVersions from '../../../src/supportedVersions.mjs'
|
import supportedVersions from '../../../src/supportedVersions.mjs'
|
||||||
|
import { ResourcesManager } from '../../../src/resourcesManager'
|
||||||
import { WorldRendererThree } from './worldrendererThree'
|
import { WorldRendererThree } from './worldrendererThree'
|
||||||
import { DocumentRenderer } from './documentRenderer'
|
import { DocumentRenderer } from './documentRenderer'
|
||||||
import { PanoramaRenderer } from './panorama'
|
import { PanoramaRenderer } from './panorama'
|
||||||
|
|
@ -12,7 +13,7 @@ import { initVR } from './world/vr'
|
||||||
|
|
||||||
// https://discourse.threejs.org/t/updates-to-color-management-in-three-js-r152/50791
|
// https://discourse.threejs.org/t/updates-to-color-management-in-three-js-r152/50791
|
||||||
THREE.ColorManagement.enabled = false
|
THREE.ColorManagement.enabled = false
|
||||||
window.THREE = THREE
|
globalThis.THREE = THREE
|
||||||
|
|
||||||
const getBackendMethods = (worldRenderer: WorldRendererThree) => {
|
const getBackendMethods = (worldRenderer: WorldRendererThree) => {
|
||||||
return {
|
return {
|
||||||
|
|
@ -24,7 +25,7 @@ const getBackendMethods = (worldRenderer: WorldRendererThree) => {
|
||||||
updatePlayerSkin: worldRenderer.entities.updatePlayerSkin.bind(worldRenderer.entities),
|
updatePlayerSkin: worldRenderer.entities.updatePlayerSkin.bind(worldRenderer.entities),
|
||||||
changeHandSwingingState: worldRenderer.changeHandSwingingState.bind(worldRenderer),
|
changeHandSwingingState: worldRenderer.changeHandSwingingState.bind(worldRenderer),
|
||||||
getHighestBlocks: worldRenderer.getHighestBlocks.bind(worldRenderer),
|
getHighestBlocks: worldRenderer.getHighestBlocks.bind(worldRenderer),
|
||||||
rerenderAllChunks: worldRenderer.rerenderAllChunks.bind(worldRenderer),
|
reloadWorld: worldRenderer.reloadWorld.bind(worldRenderer),
|
||||||
|
|
||||||
addMedia: worldRenderer.media.addMedia.bind(worldRenderer.media),
|
addMedia: worldRenderer.media.addMedia.bind(worldRenderer.media),
|
||||||
destroyMedia: worldRenderer.media.destroyMedia.bind(worldRenderer.media),
|
destroyMedia: worldRenderer.media.destroyMedia.bind(worldRenderer.media),
|
||||||
|
|
@ -43,6 +44,12 @@ const getBackendMethods = (worldRenderer: WorldRendererThree) => {
|
||||||
shakeFromDamage: worldRenderer.cameraShake.shakeFromDamage.bind(worldRenderer.cameraShake),
|
shakeFromDamage: worldRenderer.cameraShake.shakeFromDamage.bind(worldRenderer.cameraShake),
|
||||||
onPageInteraction: worldRenderer.media.onPageInteraction.bind(worldRenderer.media),
|
onPageInteraction: worldRenderer.media.onPageInteraction.bind(worldRenderer.media),
|
||||||
downloadMesherLog: worldRenderer.downloadMesherLog.bind(worldRenderer),
|
downloadMesherLog: worldRenderer.downloadMesherLog.bind(worldRenderer),
|
||||||
|
|
||||||
|
addWaypoint: worldRenderer.waypoints.addWaypoint.bind(worldRenderer.waypoints),
|
||||||
|
removeWaypoint: worldRenderer.waypoints.removeWaypoint.bind(worldRenderer.waypoints),
|
||||||
|
|
||||||
|
// New method for updating skybox
|
||||||
|
setSkyboxImage: worldRenderer.skyboxRenderer.setSkyboxImage.bind(worldRenderer.skyboxRenderer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,31 +64,27 @@ const createGraphicsBackend: GraphicsBackendLoader = (initOptions: GraphicsInitO
|
||||||
let worldRenderer: WorldRendererThree | null = null
|
let worldRenderer: WorldRendererThree | null = null
|
||||||
|
|
||||||
const startPanorama = async () => {
|
const startPanorama = async () => {
|
||||||
|
if (!documentRenderer) throw new Error('Document renderer not initialized')
|
||||||
if (worldRenderer) return
|
if (worldRenderer) return
|
||||||
const qs = new URLSearchParams(window.location.search)
|
const qs = new URLSearchParams(location.search)
|
||||||
if (qs.get('debugEntities')) {
|
if (qs.get('debugEntities')) {
|
||||||
initOptions.resourcesManager.currentConfig = { version: qs.get('version') || supportedVersions.at(-1)!, noInventoryGui: true }
|
const fullResourceManager = initOptions.resourcesManager as ResourcesManager
|
||||||
await initOptions.resourcesManager.updateAssetsData({ })
|
fullResourceManager.currentConfig = { version: qs.get('version') || supportedVersions.at(-1)!, noInventoryGui: true }
|
||||||
|
await fullResourceManager.updateAssetsData({ })
|
||||||
|
|
||||||
displayEntitiesDebugList(initOptions.resourcesManager.currentConfig.version)
|
displayEntitiesDebugList(fullResourceManager.currentConfig.version)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!panoramaRenderer) {
|
if (!panoramaRenderer) {
|
||||||
panoramaRenderer = new PanoramaRenderer(documentRenderer, initOptions, !!process.env.SINGLE_FILE_BUILD_MODE)
|
panoramaRenderer = new PanoramaRenderer(documentRenderer, initOptions, !!process.env.SINGLE_FILE_BUILD_MODE)
|
||||||
window.panoramaRenderer = panoramaRenderer
|
globalThis.panoramaRenderer = panoramaRenderer
|
||||||
callModsMethod('panoramaCreated', panoramaRenderer)
|
callModsMethod('panoramaCreated', panoramaRenderer)
|
||||||
await panoramaRenderer.start()
|
await panoramaRenderer.start()
|
||||||
callModsMethod('panoramaReady', panoramaRenderer)
|
callModsMethod('panoramaReady', panoramaRenderer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let version = ''
|
|
||||||
const prepareResources = async (ver: string, progressReporter: ProgressReporter): Promise<void> => {
|
|
||||||
version = ver
|
|
||||||
await initOptions.resourcesManager.updateAssetsData({ })
|
|
||||||
}
|
|
||||||
|
|
||||||
const startWorld = async (displayOptions: DisplayWorldOptions) => {
|
const startWorld = async (displayOptions: DisplayWorldOptions) => {
|
||||||
if (panoramaRenderer) {
|
if (panoramaRenderer) {
|
||||||
panoramaRenderer.dispose()
|
panoramaRenderer.dispose()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
import { loadSkinToCanvas } from 'skinview-utils'
|
import { loadSkinFromUsername, loadSkinImage } from '../lib/utils/skins'
|
||||||
import { loadSkinFromUsername, loadSkinImage, steveTexture } from './utils/skins'
|
import { steveTexture } from './entities'
|
||||||
|
|
||||||
|
|
||||||
export const getMyHand = async (image?: string, userName?: string) => {
|
export const getMyHand = async (image?: string, userName?: string) => {
|
||||||
let newMap: THREE.Texture
|
let newMap: THREE.Texture
|
||||||
|
|
@ -4,12 +4,12 @@ import PrismarineItem from 'prismarine-item'
|
||||||
import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
|
import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
|
||||||
import { BlockModel } from 'mc-assets'
|
import { BlockModel } from 'mc-assets'
|
||||||
import { getThreeBlockModelGroup, renderBlockThree, setBlockPosition } from '../lib/mesher/standaloneRenderer'
|
import { getThreeBlockModelGroup, renderBlockThree, setBlockPosition } from '../lib/mesher/standaloneRenderer'
|
||||||
import { getMyHand } from '../lib/hand'
|
import { MovementState, PlayerStateRenderer } from '../lib/basePlayerState'
|
||||||
import { IPlayerState, MovementState } from '../lib/basePlayerState'
|
|
||||||
import { DebugGui } from '../lib/DebugGui'
|
import { DebugGui } from '../lib/DebugGui'
|
||||||
import { SmoothSwitcher } from '../lib/smoothSwitcher'
|
import { SmoothSwitcher } from '../lib/smoothSwitcher'
|
||||||
import { watchProperty } from '../lib/utils/proxy'
|
import { watchProperty } from '../lib/utils/proxy'
|
||||||
import { WorldRendererConfig } from '../lib/worldrendererCommon'
|
import { WorldRendererConfig } from '../lib/worldrendererCommon'
|
||||||
|
import { getMyHand } from './hand'
|
||||||
import { WorldRendererThree } from './worldrendererThree'
|
import { WorldRendererThree } from './worldrendererThree'
|
||||||
import { disposeObject } from './threeJsUtils'
|
import { disposeObject } from './threeJsUtils'
|
||||||
|
|
||||||
|
|
@ -116,16 +116,20 @@ export default class HoldingBlock {
|
||||||
offHandModeLegacy = false
|
offHandModeLegacy = false
|
||||||
|
|
||||||
swingAnimator: HandSwingAnimator | undefined
|
swingAnimator: HandSwingAnimator | undefined
|
||||||
playerState: IPlayerState
|
|
||||||
config: WorldRendererConfig
|
config: WorldRendererConfig
|
||||||
|
|
||||||
constructor (public worldRenderer: WorldRendererThree, public offHand = false) {
|
constructor (public worldRenderer: WorldRendererThree, public offHand = false) {
|
||||||
this.initCameraGroup()
|
this.initCameraGroup()
|
||||||
this.playerState = worldRenderer.displayOptions.playerState
|
this.worldRenderer.onReactivePlayerStateUpdated('heldItemMain', () => {
|
||||||
this.playerState.events.on('heldItemChanged', (_, isOffHand) => {
|
if (!this.offHand) {
|
||||||
if (this.offHand !== isOffHand) return
|
this.updateItem()
|
||||||
this.updateItem()
|
}
|
||||||
})
|
}, false)
|
||||||
|
this.worldRenderer.onReactivePlayerStateUpdated('heldItemOff', () => {
|
||||||
|
if (this.offHand) {
|
||||||
|
this.updateItem()
|
||||||
|
}
|
||||||
|
}, false)
|
||||||
this.config = worldRenderer.displayOptions.inWorldRenderingConfig
|
this.config = worldRenderer.displayOptions.inWorldRenderingConfig
|
||||||
|
|
||||||
this.offHandDisplay = this.offHand
|
this.offHandDisplay = this.offHand
|
||||||
|
|
@ -134,17 +138,21 @@ export default class HoldingBlock {
|
||||||
// load default hand
|
// load default hand
|
||||||
void getMyHand().then((hand) => {
|
void getMyHand().then((hand) => {
|
||||||
this.playerHand = hand
|
this.playerHand = hand
|
||||||
|
// trigger update
|
||||||
|
this.updateItem()
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
// now watch over the player skin
|
// now watch over the player skin
|
||||||
watchProperty(
|
watchProperty(
|
||||||
async () => {
|
async () => {
|
||||||
return getMyHand(this.playerState.reactive.playerSkin, this.playerState.onlineMode ? this.playerState.username : undefined)
|
return getMyHand(this.worldRenderer.playerStateReactive.playerSkin, this.worldRenderer.playerStateReactive.onlineMode ? this.worldRenderer.playerStateReactive.username : undefined)
|
||||||
},
|
},
|
||||||
this.playerState.reactive,
|
this.worldRenderer.playerStateReactive,
|
||||||
'playerSkin',
|
'playerSkin',
|
||||||
(newHand) => {
|
(newHand) => {
|
||||||
if (newHand) {
|
if (newHand) {
|
||||||
this.playerHand = newHand
|
this.playerHand = newHand
|
||||||
|
// trigger update
|
||||||
|
this.updateItem()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(oldHand) => {
|
(oldHand) => {
|
||||||
|
|
@ -156,8 +164,8 @@ export default class HoldingBlock {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateItem () {
|
updateItem () {
|
||||||
if (!this.ready || !this.playerState.getHeldItem) return
|
if (!this.ready) return
|
||||||
const item = this.playerState.getHeldItem(this.offHand)
|
const item = this.offHand ? this.worldRenderer.playerStateReactive.heldItemOff : this.worldRenderer.playerStateReactive.heldItemMain
|
||||||
if (item) {
|
if (item) {
|
||||||
void this.setNewItem(item)
|
void this.setNewItem(item)
|
||||||
} else if (this.offHand) {
|
} else if (this.offHand) {
|
||||||
|
|
@ -347,9 +355,9 @@ export default class HoldingBlock {
|
||||||
itemId: handItem.id,
|
itemId: handItem.id,
|
||||||
}, {
|
}, {
|
||||||
'minecraft:display_context': 'firstperson',
|
'minecraft:display_context': 'firstperson',
|
||||||
'minecraft:use_duration': this.playerState.getItemUsageTicks?.(),
|
'minecraft:use_duration': this.worldRenderer.playerStateReactive.itemUsageTicks,
|
||||||
'minecraft:using_item': !!this.playerState.getItemUsageTicks?.(),
|
'minecraft:using_item': !!this.worldRenderer.playerStateReactive.itemUsageTicks,
|
||||||
}, this.lastItemModelName)
|
}, false, this.lastItemModelName)
|
||||||
if (result) {
|
if (result) {
|
||||||
const { mesh: itemMesh, isBlock, modelName } = result
|
const { mesh: itemMesh, isBlock, modelName } = result
|
||||||
if (isBlock) {
|
if (isBlock) {
|
||||||
|
|
@ -465,7 +473,7 @@ export default class HoldingBlock {
|
||||||
this.swingAnimator = new HandSwingAnimator(this.holdingBlockInnerGroup)
|
this.swingAnimator = new HandSwingAnimator(this.holdingBlockInnerGroup)
|
||||||
this.swingAnimator.type = result.type
|
this.swingAnimator.type = result.type
|
||||||
if (this.config.viewBobbing) {
|
if (this.config.viewBobbing) {
|
||||||
this.idleAnimator = new HandIdleAnimator(this.holdingBlockInnerGroup, this.playerState)
|
this.idleAnimator = new HandIdleAnimator(this.holdingBlockInnerGroup, this.worldRenderer.playerStateReactive)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -546,7 +554,7 @@ class HandIdleAnimator {
|
||||||
|
|
||||||
private readonly debugGui: DebugGui
|
private readonly debugGui: DebugGui
|
||||||
|
|
||||||
constructor (public handMesh: THREE.Object3D, public playerState: IPlayerState) {
|
constructor (public handMesh: THREE.Object3D, public playerState: PlayerStateRenderer) {
|
||||||
this.handMesh = handMesh
|
this.handMesh = handMesh
|
||||||
this.globalTime = 0
|
this.globalTime = 0
|
||||||
this.currentState = 'NOT_MOVING'
|
this.currentState = 'NOT_MOVING'
|
||||||
|
|
@ -700,7 +708,7 @@ class HandIdleAnimator {
|
||||||
|
|
||||||
// Check for state changes from player state
|
// Check for state changes from player state
|
||||||
if (this.playerState) {
|
if (this.playerState) {
|
||||||
const newState = this.playerState.getMovementState()
|
const newState = this.playerState.movementState
|
||||||
if (newState !== this.targetState) {
|
if (newState !== this.targetState) {
|
||||||
this.setState(newState)
|
this.setState(newState)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
427
renderer/viewer/three/itemMesh.ts
Normal file
427
renderer/viewer/three/itemMesh.ts
Normal file
|
|
@ -0,0 +1,427 @@
|
||||||
|
import * as THREE from 'three'
|
||||||
|
|
||||||
|
export interface Create3DItemMeshOptions {
|
||||||
|
depth: number
|
||||||
|
pixelSize?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Create3DItemMeshResult {
|
||||||
|
geometry: THREE.BufferGeometry
|
||||||
|
totalVertices: number
|
||||||
|
totalTriangles: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a 3D item geometry with front/back faces and connecting edges
|
||||||
|
* from a canvas containing the item texture
|
||||||
|
*/
|
||||||
|
export function create3DItemMesh (
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
options: Create3DItemMeshOptions
|
||||||
|
): Create3DItemMeshResult {
|
||||||
|
const { depth, pixelSize } = options
|
||||||
|
|
||||||
|
// Validate canvas dimensions
|
||||||
|
if (canvas.width <= 0 || canvas.height <= 0) {
|
||||||
|
throw new Error(`Invalid canvas dimensions: ${canvas.width}x${canvas.height}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d')!
|
||||||
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
|
||||||
|
const { data } = imageData
|
||||||
|
|
||||||
|
const w = canvas.width
|
||||||
|
const h = canvas.height
|
||||||
|
const halfDepth = depth / 2
|
||||||
|
const actualPixelSize = pixelSize ?? (1 / Math.max(w, h))
|
||||||
|
|
||||||
|
// Find opaque pixels
|
||||||
|
const isOpaque = (x: number, y: number) => {
|
||||||
|
if (x < 0 || y < 0 || x >= w || y >= h) return false
|
||||||
|
const i = (y * w + x) * 4
|
||||||
|
return data[i + 3] > 128 // alpha > 128
|
||||||
|
}
|
||||||
|
|
||||||
|
const vertices: number[] = []
|
||||||
|
const indices: number[] = []
|
||||||
|
const uvs: number[] = []
|
||||||
|
const normals: number[] = []
|
||||||
|
|
||||||
|
let vertexIndex = 0
|
||||||
|
|
||||||
|
// Helper to add a vertex
|
||||||
|
const addVertex = (x: number, y: number, z: number, u: number, v: number, nx: number, ny: number, nz: number) => {
|
||||||
|
vertices.push(x, y, z)
|
||||||
|
uvs.push(u, v)
|
||||||
|
normals.push(nx, ny, nz)
|
||||||
|
return vertexIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to add a quad (two triangles)
|
||||||
|
const addQuad = (v0: number, v1: number, v2: number, v3: number) => {
|
||||||
|
indices.push(v0, v1, v2, v0, v2, v3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert pixel coordinates to world coordinates
|
||||||
|
const pixelToWorld = (px: number, py: number) => {
|
||||||
|
const x = (px / w - 0.5) * actualPixelSize * w
|
||||||
|
const y = -(py / h - 0.5) * actualPixelSize * h
|
||||||
|
return { x, y }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a grid of vertices for front and back faces
|
||||||
|
const frontVertices: Array<Array<number | null>> = Array.from({ length: h + 1 }, () => Array.from({ length: w + 1 }, () => null))
|
||||||
|
const backVertices: Array<Array<number | null>> = Array.from({ length: h + 1 }, () => Array.from({ length: w + 1 }, () => null))
|
||||||
|
|
||||||
|
// Create vertices at pixel corners
|
||||||
|
for (let py = 0; py <= h; py++) {
|
||||||
|
for (let px = 0; px <= w; px++) {
|
||||||
|
const { x, y } = pixelToWorld(px - 0.5, py - 0.5)
|
||||||
|
|
||||||
|
// UV coordinates should map to the texture space of the extracted tile
|
||||||
|
const u = px / w
|
||||||
|
const v = py / h
|
||||||
|
|
||||||
|
// Check if this vertex is needed for any face or edge
|
||||||
|
let needVertex = false
|
||||||
|
|
||||||
|
// Check all 4 adjacent pixels to see if any are opaque
|
||||||
|
const adjacentPixels = [
|
||||||
|
[px - 1, py - 1], // top-left pixel
|
||||||
|
[px, py - 1], // top-right pixel
|
||||||
|
[px - 1, py], // bottom-left pixel
|
||||||
|
[px, py] // bottom-right pixel
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const [adjX, adjY] of adjacentPixels) {
|
||||||
|
if (isOpaque(adjX, adjY)) {
|
||||||
|
needVertex = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needVertex) {
|
||||||
|
frontVertices[py][px] = addVertex(x, y, halfDepth, u, v, 0, 0, 1)
|
||||||
|
backVertices[py][px] = addVertex(x, y, -halfDepth, u, v, 0, 0, -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create front and back faces
|
||||||
|
for (let py = 0; py < h; py++) {
|
||||||
|
for (let px = 0; px < w; px++) {
|
||||||
|
if (!isOpaque(px, py)) continue
|
||||||
|
|
||||||
|
const v00 = frontVertices[py][px]
|
||||||
|
const v10 = frontVertices[py][px + 1]
|
||||||
|
const v11 = frontVertices[py + 1][px + 1]
|
||||||
|
const v01 = frontVertices[py + 1][px]
|
||||||
|
|
||||||
|
const b00 = backVertices[py][px]
|
||||||
|
const b10 = backVertices[py][px + 1]
|
||||||
|
const b11 = backVertices[py + 1][px + 1]
|
||||||
|
const b01 = backVertices[py + 1][px]
|
||||||
|
|
||||||
|
if (v00 !== null && v10 !== null && v11 !== null && v01 !== null) {
|
||||||
|
// Front face
|
||||||
|
addQuad(v00, v10, v11, v01)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b00 !== null && b10 !== null && b11 !== null && b01 !== null) {
|
||||||
|
// Back face (reversed winding)
|
||||||
|
addQuad(b10, b00, b01, b11)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create edge faces for each side of the pixel with proper UVs
|
||||||
|
for (let py = 0; py < h; py++) {
|
||||||
|
for (let px = 0; px < w; px++) {
|
||||||
|
if (!isOpaque(px, py)) continue
|
||||||
|
|
||||||
|
const pixelU = (px + 0.5) / w // Center of current pixel
|
||||||
|
const pixelV = (py + 0.5) / h
|
||||||
|
|
||||||
|
// Left edge (x = px)
|
||||||
|
if (!isOpaque(px - 1, py)) {
|
||||||
|
const f0 = frontVertices[py][px]
|
||||||
|
const f1 = frontVertices[py + 1][px]
|
||||||
|
const b0 = backVertices[py][px]
|
||||||
|
const b1 = backVertices[py + 1][px]
|
||||||
|
|
||||||
|
if (f0 !== null && f1 !== null && b0 !== null && b1 !== null) {
|
||||||
|
// Create new vertices for edge with current pixel's UV
|
||||||
|
const ef0 = addVertex(vertices[f0 * 3], vertices[f0 * 3 + 1], vertices[f0 * 3 + 2], pixelU, pixelV, -1, 0, 0)
|
||||||
|
const ef1 = addVertex(vertices[f1 * 3], vertices[f1 * 3 + 1], vertices[f1 * 3 + 2], pixelU, pixelV, -1, 0, 0)
|
||||||
|
const eb1 = addVertex(vertices[b1 * 3], vertices[b1 * 3 + 1], vertices[b1 * 3 + 2], pixelU, pixelV, -1, 0, 0)
|
||||||
|
const eb0 = addVertex(vertices[b0 * 3], vertices[b0 * 3 + 1], vertices[b0 * 3 + 2], pixelU, pixelV, -1, 0, 0)
|
||||||
|
addQuad(ef0, ef1, eb1, eb0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right edge (x = px + 1)
|
||||||
|
if (!isOpaque(px + 1, py)) {
|
||||||
|
const f0 = frontVertices[py + 1][px + 1]
|
||||||
|
const f1 = frontVertices[py][px + 1]
|
||||||
|
const b0 = backVertices[py + 1][px + 1]
|
||||||
|
const b1 = backVertices[py][px + 1]
|
||||||
|
|
||||||
|
if (f0 !== null && f1 !== null && b0 !== null && b1 !== null) {
|
||||||
|
const ef0 = addVertex(vertices[f0 * 3], vertices[f0 * 3 + 1], vertices[f0 * 3 + 2], pixelU, pixelV, 1, 0, 0)
|
||||||
|
const ef1 = addVertex(vertices[f1 * 3], vertices[f1 * 3 + 1], vertices[f1 * 3 + 2], pixelU, pixelV, 1, 0, 0)
|
||||||
|
const eb1 = addVertex(vertices[b1 * 3], vertices[b1 * 3 + 1], vertices[b1 * 3 + 2], pixelU, pixelV, 1, 0, 0)
|
||||||
|
const eb0 = addVertex(vertices[b0 * 3], vertices[b0 * 3 + 1], vertices[b0 * 3 + 2], pixelU, pixelV, 1, 0, 0)
|
||||||
|
addQuad(ef0, ef1, eb1, eb0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top edge (y = py)
|
||||||
|
if (!isOpaque(px, py - 1)) {
|
||||||
|
const f0 = frontVertices[py][px]
|
||||||
|
const f1 = frontVertices[py][px + 1]
|
||||||
|
const b0 = backVertices[py][px]
|
||||||
|
const b1 = backVertices[py][px + 1]
|
||||||
|
|
||||||
|
if (f0 !== null && f1 !== null && b0 !== null && b1 !== null) {
|
||||||
|
const ef0 = addVertex(vertices[f0 * 3], vertices[f0 * 3 + 1], vertices[f0 * 3 + 2], pixelU, pixelV, 0, -1, 0)
|
||||||
|
const ef1 = addVertex(vertices[f1 * 3], vertices[f1 * 3 + 1], vertices[f1 * 3 + 2], pixelU, pixelV, 0, -1, 0)
|
||||||
|
const eb1 = addVertex(vertices[b1 * 3], vertices[b1 * 3 + 1], vertices[b1 * 3 + 2], pixelU, pixelV, 0, -1, 0)
|
||||||
|
const eb0 = addVertex(vertices[b0 * 3], vertices[b0 * 3 + 1], vertices[b0 * 3 + 2], pixelU, pixelV, 0, -1, 0)
|
||||||
|
addQuad(ef0, ef1, eb1, eb0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom edge (y = py + 1)
|
||||||
|
if (!isOpaque(px, py + 1)) {
|
||||||
|
const f0 = frontVertices[py + 1][px + 1]
|
||||||
|
const f1 = frontVertices[py + 1][px]
|
||||||
|
const b0 = backVertices[py + 1][px + 1]
|
||||||
|
const b1 = backVertices[py + 1][px]
|
||||||
|
|
||||||
|
if (f0 !== null && f1 !== null && b0 !== null && b1 !== null) {
|
||||||
|
const ef0 = addVertex(vertices[f0 * 3], vertices[f0 * 3 + 1], vertices[f0 * 3 + 2], pixelU, pixelV, 0, 1, 0)
|
||||||
|
const ef1 = addVertex(vertices[f1 * 3], vertices[f1 * 3 + 1], vertices[f1 * 3 + 2], pixelU, pixelV, 0, 1, 0)
|
||||||
|
const eb1 = addVertex(vertices[b1 * 3], vertices[b1 * 3 + 1], vertices[b1 * 3 + 2], pixelU, pixelV, 0, 1, 0)
|
||||||
|
const eb0 = addVertex(vertices[b0 * 3], vertices[b0 * 3 + 1], vertices[b0 * 3 + 2], pixelU, pixelV, 0, 1, 0)
|
||||||
|
addQuad(ef0, ef1, eb1, eb0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const geometry = new THREE.BufferGeometry()
|
||||||
|
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3))
|
||||||
|
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2))
|
||||||
|
geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3))
|
||||||
|
geometry.setIndex(indices)
|
||||||
|
|
||||||
|
// Compute normals properly
|
||||||
|
geometry.computeVertexNormals()
|
||||||
|
|
||||||
|
return {
|
||||||
|
geometry,
|
||||||
|
totalVertices: vertexIndex,
|
||||||
|
totalTriangles: indices.length / 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ItemTextureInfo {
|
||||||
|
u: number
|
||||||
|
v: number
|
||||||
|
sizeX: number
|
||||||
|
sizeY: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ItemMeshResult {
|
||||||
|
mesh: THREE.Object3D
|
||||||
|
itemsTexture?: THREE.Texture
|
||||||
|
itemsTextureFlipped?: THREE.Texture
|
||||||
|
cleanup?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts item texture region to a canvas
|
||||||
|
*/
|
||||||
|
export function extractItemTextureToCanvas (
|
||||||
|
sourceTexture: THREE.Texture,
|
||||||
|
textureInfo: ItemTextureInfo
|
||||||
|
): HTMLCanvasElement {
|
||||||
|
const { u, v, sizeX, sizeY } = textureInfo
|
||||||
|
|
||||||
|
// Calculate canvas size - fix the calculation
|
||||||
|
const canvasWidth = Math.max(1, Math.floor(sizeX * sourceTexture.image.width))
|
||||||
|
const canvasHeight = Math.max(1, Math.floor(sizeY * sourceTexture.image.height))
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = canvasWidth
|
||||||
|
canvas.height = canvasHeight
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d')!
|
||||||
|
ctx.imageSmoothingEnabled = false
|
||||||
|
|
||||||
|
// Draw the item texture region to canvas
|
||||||
|
ctx.drawImage(
|
||||||
|
sourceTexture.image,
|
||||||
|
u * sourceTexture.image.width,
|
||||||
|
v * sourceTexture.image.height,
|
||||||
|
sizeX * sourceTexture.image.width,
|
||||||
|
sizeY * sourceTexture.image.height,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
canvas.width,
|
||||||
|
canvas.height
|
||||||
|
)
|
||||||
|
|
||||||
|
return canvas
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates either a 2D or 3D item mesh based on parameters
|
||||||
|
*/
|
||||||
|
export function createItemMesh (
|
||||||
|
sourceTexture: THREE.Texture,
|
||||||
|
textureInfo: ItemTextureInfo,
|
||||||
|
options: {
|
||||||
|
faceCamera?: boolean
|
||||||
|
use3D?: boolean
|
||||||
|
depth?: number
|
||||||
|
} = {}
|
||||||
|
): ItemMeshResult {
|
||||||
|
const { faceCamera = false, use3D = true, depth = 0.04 } = options
|
||||||
|
const { u, v, sizeX, sizeY } = textureInfo
|
||||||
|
|
||||||
|
if (faceCamera) {
|
||||||
|
// Create sprite for camera-facing items
|
||||||
|
const itemsTexture = sourceTexture.clone()
|
||||||
|
itemsTexture.flipY = true
|
||||||
|
itemsTexture.offset.set(u, 1 - v - sizeY)
|
||||||
|
itemsTexture.repeat.set(sizeX, sizeY)
|
||||||
|
itemsTexture.needsUpdate = true
|
||||||
|
itemsTexture.magFilter = THREE.NearestFilter
|
||||||
|
itemsTexture.minFilter = THREE.NearestFilter
|
||||||
|
|
||||||
|
const spriteMat = new THREE.SpriteMaterial({
|
||||||
|
map: itemsTexture,
|
||||||
|
transparent: true,
|
||||||
|
alphaTest: 0.1,
|
||||||
|
})
|
||||||
|
const mesh = new THREE.Sprite(spriteMat)
|
||||||
|
|
||||||
|
return {
|
||||||
|
mesh,
|
||||||
|
itemsTexture,
|
||||||
|
cleanup () {
|
||||||
|
itemsTexture.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (use3D) {
|
||||||
|
// Try to create 3D mesh
|
||||||
|
try {
|
||||||
|
const canvas = extractItemTextureToCanvas(sourceTexture, textureInfo)
|
||||||
|
const { geometry } = create3DItemMesh(canvas, { depth })
|
||||||
|
|
||||||
|
// Create texture from canvas for the 3D mesh
|
||||||
|
const itemsTexture = new THREE.CanvasTexture(canvas)
|
||||||
|
itemsTexture.magFilter = THREE.NearestFilter
|
||||||
|
itemsTexture.minFilter = THREE.NearestFilter
|
||||||
|
itemsTexture.wrapS = itemsTexture.wrapT = THREE.ClampToEdgeWrapping
|
||||||
|
itemsTexture.flipY = false
|
||||||
|
itemsTexture.needsUpdate = true
|
||||||
|
|
||||||
|
const material = new THREE.MeshStandardMaterial({
|
||||||
|
map: itemsTexture,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
transparent: true,
|
||||||
|
alphaTest: 0.1,
|
||||||
|
})
|
||||||
|
|
||||||
|
const mesh = new THREE.Mesh(geometry, material)
|
||||||
|
|
||||||
|
return {
|
||||||
|
mesh,
|
||||||
|
itemsTexture,
|
||||||
|
cleanup () {
|
||||||
|
itemsTexture.dispose()
|
||||||
|
geometry.dispose()
|
||||||
|
if (material.map) material.map.dispose()
|
||||||
|
material.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to create 3D item mesh, falling back to 2D:', error)
|
||||||
|
// Fall through to 2D rendering
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to 2D flat rendering
|
||||||
|
const itemsTexture = sourceTexture.clone()
|
||||||
|
itemsTexture.flipY = true
|
||||||
|
itemsTexture.offset.set(u, 1 - v - sizeY)
|
||||||
|
itemsTexture.repeat.set(sizeX, sizeY)
|
||||||
|
itemsTexture.needsUpdate = true
|
||||||
|
itemsTexture.magFilter = THREE.NearestFilter
|
||||||
|
itemsTexture.minFilter = THREE.NearestFilter
|
||||||
|
|
||||||
|
const itemsTextureFlipped = itemsTexture.clone()
|
||||||
|
itemsTextureFlipped.repeat.x *= -1
|
||||||
|
itemsTextureFlipped.needsUpdate = true
|
||||||
|
itemsTextureFlipped.offset.set(u + sizeX, 1 - v - sizeY)
|
||||||
|
|
||||||
|
const material = new THREE.MeshStandardMaterial({
|
||||||
|
map: itemsTexture,
|
||||||
|
transparent: true,
|
||||||
|
alphaTest: 0.1,
|
||||||
|
})
|
||||||
|
const materialFlipped = new THREE.MeshStandardMaterial({
|
||||||
|
map: itemsTextureFlipped,
|
||||||
|
transparent: true,
|
||||||
|
alphaTest: 0.1,
|
||||||
|
})
|
||||||
|
const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 0), [
|
||||||
|
new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), new THREE.MeshBasicMaterial({ color: 0x00_00_00 }),
|
||||||
|
new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), new THREE.MeshBasicMaterial({ color: 0x00_00_00 }),
|
||||||
|
material, materialFlipped,
|
||||||
|
])
|
||||||
|
|
||||||
|
return {
|
||||||
|
mesh,
|
||||||
|
itemsTexture,
|
||||||
|
itemsTextureFlipped,
|
||||||
|
cleanup () {
|
||||||
|
itemsTexture.dispose()
|
||||||
|
itemsTextureFlipped.dispose()
|
||||||
|
material.dispose()
|
||||||
|
materialFlipped.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a complete 3D item mesh from a canvas texture
|
||||||
|
*/
|
||||||
|
export function createItemMeshFromCanvas (
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
options: Create3DItemMeshOptions
|
||||||
|
): THREE.Mesh {
|
||||||
|
const { geometry } = create3DItemMesh(canvas, options)
|
||||||
|
|
||||||
|
// Base color texture for the item
|
||||||
|
const colorTexture = new THREE.CanvasTexture(canvas)
|
||||||
|
colorTexture.magFilter = THREE.NearestFilter
|
||||||
|
colorTexture.minFilter = THREE.NearestFilter
|
||||||
|
colorTexture.wrapS = colorTexture.wrapT = THREE.ClampToEdgeWrapping
|
||||||
|
colorTexture.flipY = false // Important for canvas textures
|
||||||
|
colorTexture.needsUpdate = true
|
||||||
|
|
||||||
|
// Material - no transparency, no alpha test needed for edges
|
||||||
|
const material = new THREE.MeshBasicMaterial({
|
||||||
|
map: colorTexture,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
transparent: true,
|
||||||
|
alphaTest: 0.1
|
||||||
|
})
|
||||||
|
|
||||||
|
return new THREE.Mesh(geometry, material)
|
||||||
|
}
|
||||||
|
|
@ -6,11 +6,14 @@ import * as tweenJs from '@tweenjs/tween.js'
|
||||||
import type { GraphicsInitOptions } from '../../../src/appViewer'
|
import type { GraphicsInitOptions } from '../../../src/appViewer'
|
||||||
import { WorldDataEmitter } from '../lib/worldDataEmitter'
|
import { WorldDataEmitter } from '../lib/worldDataEmitter'
|
||||||
import { defaultWorldRendererConfig, WorldRendererCommon } from '../lib/worldrendererCommon'
|
import { defaultWorldRendererConfig, WorldRendererCommon } from '../lib/worldrendererCommon'
|
||||||
import { BasePlayerState } from '../lib/basePlayerState'
|
|
||||||
import { getDefaultRendererState } from '../baseGraphicsBackend'
|
import { getDefaultRendererState } from '../baseGraphicsBackend'
|
||||||
|
import { ResourcesManager } from '../../../src/resourcesManager'
|
||||||
|
import { getInitialPlayerStateRenderer } from '../lib/basePlayerState'
|
||||||
|
import { loadThreeJsTextureFromUrl, loadThreeJsTextureFromUrlSync } from './threeJsUtils'
|
||||||
import { WorldRendererThree } from './worldrendererThree'
|
import { WorldRendererThree } from './worldrendererThree'
|
||||||
import { EntityMesh } from './entity/EntityMesh'
|
import { EntityMesh } from './entity/EntityMesh'
|
||||||
import { DocumentRenderer } from './documentRenderer'
|
import { DocumentRenderer } from './documentRenderer'
|
||||||
|
import { PANORAMA_VERSION } from './panoramaShared'
|
||||||
|
|
||||||
const panoramaFiles = [
|
const panoramaFiles = [
|
||||||
'panorama_3.png', // right (+x)
|
'panorama_3.png', // right (+x)
|
||||||
|
|
@ -48,7 +51,7 @@ export class PanoramaRenderer {
|
||||||
this.directionalLight.castShadow = true
|
this.directionalLight.castShadow = true
|
||||||
this.scene.add(this.directionalLight)
|
this.scene.add(this.directionalLight)
|
||||||
|
|
||||||
this.camera = new THREE.PerspectiveCamera(85, window.innerWidth / window.innerHeight, 0.05, 1000)
|
this.camera = new THREE.PerspectiveCamera(85, this.documentRenderer.canvas.width / this.documentRenderer.canvas.height, 0.05, 1000)
|
||||||
this.camera.position.set(0, 0, 0)
|
this.camera.position.set(0, 0, 0)
|
||||||
this.camera.rotation.set(0, 0, 0)
|
this.camera.rotation.set(0, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
@ -63,47 +66,57 @@ export class PanoramaRenderer {
|
||||||
|
|
||||||
this.documentRenderer.render = (sizeChanged = false) => {
|
this.documentRenderer.render = (sizeChanged = false) => {
|
||||||
if (sizeChanged) {
|
if (sizeChanged) {
|
||||||
this.camera.aspect = window.innerWidth / window.innerHeight
|
this.camera.aspect = this.documentRenderer.canvas.width / this.documentRenderer.canvas.height
|
||||||
this.camera.updateProjectionMatrix()
|
this.camera.updateProjectionMatrix()
|
||||||
}
|
}
|
||||||
this.documentRenderer.renderer.render(this.scene, this.camera)
|
this.documentRenderer.renderer.render(this.scene, this.camera)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async debugImageInFrontOfCamera () {
|
||||||
|
const image = await loadThreeJsTextureFromUrl(join('background', 'panorama_0.png'))
|
||||||
|
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1000, 1000), new THREE.MeshBasicMaterial({ map: image }))
|
||||||
|
mesh.position.set(0, 0, -500)
|
||||||
|
mesh.rotation.set(0, 0, 0)
|
||||||
|
this.scene.add(mesh)
|
||||||
|
}
|
||||||
|
|
||||||
addClassicPanorama () {
|
addClassicPanorama () {
|
||||||
const panorGeo = new THREE.BoxGeometry(1000, 1000, 1000)
|
const panorGeo = new THREE.BoxGeometry(1000, 1000, 1000)
|
||||||
const loader = new THREE.TextureLoader()
|
|
||||||
const panorMaterials = [] as THREE.MeshBasicMaterial[]
|
const panorMaterials = [] as THREE.MeshBasicMaterial[]
|
||||||
const fadeInDuration = 200
|
const fadeInDuration = 200
|
||||||
|
|
||||||
for (const file of panoramaFiles) {
|
// void this.debugImageInFrontOfCamera()
|
||||||
// eslint-disable-next-line prefer-const
|
|
||||||
let material: THREE.MeshBasicMaterial
|
for (const file of panoramaFiles) {
|
||||||
|
const load = async () => {
|
||||||
|
const { texture } = loadThreeJsTextureFromUrlSync(join('background', file))
|
||||||
|
|
||||||
|
// Instead of using repeat/offset to flip, we'll use the texture matrix
|
||||||
|
texture.matrixAutoUpdate = false
|
||||||
|
texture.matrix.set(
|
||||||
|
-1, 0, 1, 0, 1, 0, 0, 0, 1
|
||||||
|
)
|
||||||
|
|
||||||
|
texture.wrapS = THREE.ClampToEdgeWrapping
|
||||||
|
texture.wrapT = THREE.ClampToEdgeWrapping
|
||||||
|
texture.minFilter = THREE.LinearFilter
|
||||||
|
texture.magFilter = THREE.LinearFilter
|
||||||
|
|
||||||
|
const material = new THREE.MeshBasicMaterial({
|
||||||
|
map: texture,
|
||||||
|
transparent: true,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
depthWrite: false,
|
||||||
|
opacity: 0 // Start with 0 opacity
|
||||||
|
})
|
||||||
|
|
||||||
const texture = loader.load(join('background', file), () => {
|
|
||||||
// Start fade-in when texture is loaded
|
// Start fade-in when texture is loaded
|
||||||
this.startTimes.set(material, Date.now())
|
this.startTimes.set(material, Date.now())
|
||||||
})
|
panorMaterials.push(material)
|
||||||
|
}
|
||||||
|
|
||||||
// Instead of using repeat/offset to flip, we'll use the texture matrix
|
void load()
|
||||||
texture.matrixAutoUpdate = false
|
|
||||||
texture.matrix.set(
|
|
||||||
-1, 0, 1, 0, 1, 0, 0, 0, 1
|
|
||||||
)
|
|
||||||
|
|
||||||
texture.wrapS = THREE.ClampToEdgeWrapping
|
|
||||||
texture.wrapT = THREE.ClampToEdgeWrapping
|
|
||||||
texture.minFilter = THREE.LinearFilter
|
|
||||||
texture.magFilter = THREE.LinearFilter
|
|
||||||
|
|
||||||
material = new THREE.MeshBasicMaterial({
|
|
||||||
map: texture,
|
|
||||||
transparent: true,
|
|
||||||
side: THREE.DoubleSide,
|
|
||||||
depthWrite: false,
|
|
||||||
opacity: 0 // Start with 0 opacity
|
|
||||||
})
|
|
||||||
panorMaterials.push(material)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const panoramaBox = new THREE.Mesh(panorGeo, panorMaterials)
|
const panoramaBox = new THREE.Mesh(panorGeo, panorMaterials)
|
||||||
|
|
@ -144,9 +157,10 @@ export class PanoramaRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
async worldBlocksPanorama () {
|
async worldBlocksPanorama () {
|
||||||
const version = '1.21.4'
|
const version = PANORAMA_VERSION
|
||||||
this.options.resourcesManager.currentConfig = { version, noInventoryGui: true, }
|
const fullResourceManager = this.options.resourcesManager as ResourcesManager
|
||||||
await this.options.resourcesManager.updateAssetsData({ })
|
fullResourceManager.currentConfig = { version, noInventoryGui: true, }
|
||||||
|
await fullResourceManager.updateAssetsData({ })
|
||||||
if (this.abortController.signal.aborted) return
|
if (this.abortController.signal.aborted) return
|
||||||
console.time('load panorama scene')
|
console.time('load panorama scene')
|
||||||
const world = getSyncWorld(version)
|
const world = getSyncWorld(version)
|
||||||
|
|
@ -184,9 +198,9 @@ export class PanoramaRenderer {
|
||||||
version,
|
version,
|
||||||
worldView,
|
worldView,
|
||||||
inWorldRenderingConfig: defaultWorldRendererConfig,
|
inWorldRenderingConfig: defaultWorldRendererConfig,
|
||||||
playerState: new BasePlayerState(),
|
playerStateReactive: getInitialPlayerStateRenderer().reactive,
|
||||||
rendererState: getDefaultRendererState(),
|
rendererState: getDefaultRendererState().reactive,
|
||||||
nonReactiveState: getDefaultRendererState()
|
nonReactiveState: getDefaultRendererState().nonReactive
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (this.worldRenderer instanceof WorldRendererThree) {
|
if (this.worldRenderer instanceof WorldRendererThree) {
|
||||||
|
|
|
||||||
1
renderer/viewer/three/panoramaShared.ts
Normal file
1
renderer/viewer/three/panoramaShared.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export const PANORAMA_VERSION = '1.21.4'
|
||||||
82
renderer/viewer/three/renderSlot.ts
Normal file
82
renderer/viewer/three/renderSlot.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { getRenamedData } from 'flying-squid/dist/blockRenames'
|
||||||
|
import { BlockModel } from 'mc-assets'
|
||||||
|
import { versionToNumber } from 'mc-assets/dist/utils'
|
||||||
|
import type { ResourcesManagerCommon } from '../../../src/resourcesManager'
|
||||||
|
|
||||||
|
export type ResolvedItemModelRender = {
|
||||||
|
modelName: string,
|
||||||
|
originalItemName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const renderSlot = (model: ResolvedItemModelRender, resourcesManager: ResourcesManagerCommon, debugIsQuickbar = false, fullBlockModelSupport = false): {
|
||||||
|
texture: string,
|
||||||
|
blockData: Record<string, { slice, path }> & { resolvedModel: BlockModel } | null,
|
||||||
|
scale: number | null,
|
||||||
|
slice: number[] | null,
|
||||||
|
modelName: string | null,
|
||||||
|
} => {
|
||||||
|
let itemModelName = model.modelName
|
||||||
|
const isItem = loadedData.itemsByName[itemModelName]
|
||||||
|
|
||||||
|
// #region normalize item name
|
||||||
|
if (versionToNumber(bot.version) < versionToNumber('1.13')) itemModelName = getRenamedData(isItem ? 'items' : 'blocks', itemModelName, bot.version, '1.13.1') as string
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
|
||||||
|
let itemTexture
|
||||||
|
|
||||||
|
if (!fullBlockModelSupport) {
|
||||||
|
const atlas = resourcesManager.currentResources?.guiAtlas?.json
|
||||||
|
// todo atlas holds all rendered blocks, not all possibly rendered item/block models, need to request this on demand instead (this is how vanilla works)
|
||||||
|
const tryGetAtlasTexture = (name?: string) => name && atlas?.textures[name.replace('minecraft:', '').replace('block/', '').replace('blocks/', '').replace('item/', '').replace('items/', '').replace('_inventory', '')]
|
||||||
|
const item = tryGetAtlasTexture(itemModelName) ?? tryGetAtlasTexture(model.originalItemName)
|
||||||
|
if (item) {
|
||||||
|
const x = item.u * atlas.width
|
||||||
|
const y = item.v * atlas.height
|
||||||
|
return {
|
||||||
|
texture: 'gui',
|
||||||
|
slice: [x, y, atlas.tileSize, atlas.tileSize],
|
||||||
|
scale: 0.25,
|
||||||
|
blockData: null,
|
||||||
|
modelName: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockToTopTexture = (r) => r.top ?? r
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!appViewer.resourcesManager.currentResources?.itemsRenderer) throw new Error('Items renderer is not available')
|
||||||
|
itemTexture =
|
||||||
|
appViewer.resourcesManager.currentResources.itemsRenderer.getItemTexture(itemModelName, {}, false, fullBlockModelSupport)
|
||||||
|
?? (model.originalItemName ? appViewer.resourcesManager.currentResources.itemsRenderer.getItemTexture(model.originalItemName, {}, false, fullBlockModelSupport) : undefined)
|
||||||
|
?? appViewer.resourcesManager.currentResources.itemsRenderer.getItemTexture('item/missing_texture')!
|
||||||
|
} catch (err) {
|
||||||
|
// get resourcepack from resource manager
|
||||||
|
reportError?.(`Failed to render item ${itemModelName} (original: ${model.originalItemName}) on ${bot.version} (resourcepack: TODO!): ${err.stack}`)
|
||||||
|
itemTexture = blockToTopTexture(appViewer.resourcesManager.currentResources!.itemsRenderer.getItemTexture('errored')!)
|
||||||
|
}
|
||||||
|
|
||||||
|
itemTexture ??= blockToTopTexture(appViewer.resourcesManager.currentResources!.itemsRenderer.getItemTexture('unknown')!)
|
||||||
|
|
||||||
|
|
||||||
|
if ('type' in itemTexture) {
|
||||||
|
// is item
|
||||||
|
return {
|
||||||
|
texture: itemTexture.type,
|
||||||
|
slice: itemTexture.slice,
|
||||||
|
modelName: itemModelName,
|
||||||
|
blockData: null,
|
||||||
|
scale: null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// is block
|
||||||
|
return {
|
||||||
|
texture: 'blocks',
|
||||||
|
blockData: itemTexture,
|
||||||
|
modelName: itemModelName,
|
||||||
|
slice: null,
|
||||||
|
scale: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
406
renderer/viewer/three/skyboxRenderer.ts
Normal file
406
renderer/viewer/three/skyboxRenderer.ts
Normal file
|
|
@ -0,0 +1,406 @@
|
||||||
|
import * as THREE from 'three'
|
||||||
|
import { DebugGui } from '../lib/DebugGui'
|
||||||
|
|
||||||
|
export const DEFAULT_TEMPERATURE = 0.75
|
||||||
|
|
||||||
|
export class SkyboxRenderer {
|
||||||
|
private texture: THREE.Texture | null = null
|
||||||
|
private mesh: THREE.Mesh<THREE.SphereGeometry, THREE.MeshBasicMaterial> | null = null
|
||||||
|
private skyMesh: THREE.Mesh | null = null
|
||||||
|
private voidMesh: THREE.Mesh | null = null
|
||||||
|
|
||||||
|
// World state
|
||||||
|
private worldTime = 0
|
||||||
|
private partialTicks = 0
|
||||||
|
private viewDistance = 4
|
||||||
|
private temperature = DEFAULT_TEMPERATURE
|
||||||
|
private inWater = false
|
||||||
|
private waterBreathing = false
|
||||||
|
private fogBrightness = 0
|
||||||
|
private prevFogBrightness = 0
|
||||||
|
private readonly fogOrangeness = 0 // Debug property to control sky color orangeness
|
||||||
|
private readonly distanceFactor = 2.7
|
||||||
|
|
||||||
|
private readonly brightnessAtPosition = 1
|
||||||
|
debugGui: DebugGui
|
||||||
|
|
||||||
|
constructor (private readonly scene: THREE.Scene, public defaultSkybox: boolean, public initialImage: string | null) {
|
||||||
|
this.debugGui = new DebugGui('skybox_renderer', this, [
|
||||||
|
'temperature',
|
||||||
|
'worldTime',
|
||||||
|
'inWater',
|
||||||
|
'waterBreathing',
|
||||||
|
'fogOrangeness',
|
||||||
|
'brightnessAtPosition',
|
||||||
|
'distanceFactor'
|
||||||
|
], {
|
||||||
|
brightnessAtPosition: { min: 0, max: 1, step: 0.01 },
|
||||||
|
temperature: { min: 0, max: 1, step: 0.01 },
|
||||||
|
worldTime: { min: 0, max: 24_000, step: 1 },
|
||||||
|
fogOrangeness: { min: -1, max: 1, step: 0.01 },
|
||||||
|
distanceFactor: { min: 0, max: 5, step: 0.01 },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!initialImage) {
|
||||||
|
this.createGradientSky()
|
||||||
|
}
|
||||||
|
// this.debugGui.activate()
|
||||||
|
}
|
||||||
|
|
||||||
|
async init () {
|
||||||
|
if (this.initialImage) {
|
||||||
|
await this.setSkyboxImage(this.initialImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setSkyboxImage (imageUrl: string) {
|
||||||
|
// Dispose old textures if they exist
|
||||||
|
if (this.texture) {
|
||||||
|
this.texture.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the equirectangular texture
|
||||||
|
const textureLoader = new THREE.TextureLoader()
|
||||||
|
this.texture = await new Promise((resolve) => {
|
||||||
|
textureLoader.load(
|
||||||
|
imageUrl,
|
||||||
|
(texture) => {
|
||||||
|
texture.mapping = THREE.EquirectangularReflectionMapping
|
||||||
|
texture.encoding = THREE.sRGBEncoding
|
||||||
|
// Keep pixelated look
|
||||||
|
texture.minFilter = THREE.NearestFilter
|
||||||
|
texture.magFilter = THREE.NearestFilter
|
||||||
|
texture.needsUpdate = true
|
||||||
|
resolve(texture)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create or update the skybox
|
||||||
|
if (this.mesh) {
|
||||||
|
// Just update the texture on the existing material
|
||||||
|
this.mesh.material.map = this.texture
|
||||||
|
this.mesh.material.needsUpdate = true
|
||||||
|
} else {
|
||||||
|
// Create a large sphere geometry for the skybox
|
||||||
|
const geometry = new THREE.SphereGeometry(500, 60, 40)
|
||||||
|
// Flip the geometry inside out
|
||||||
|
geometry.scale(-1, 1, 1)
|
||||||
|
|
||||||
|
// Create material using the loaded texture
|
||||||
|
const material = new THREE.MeshBasicMaterial({
|
||||||
|
map: this.texture,
|
||||||
|
side: THREE.FrontSide // Changed to FrontSide since we're flipping the geometry
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create and add the skybox mesh
|
||||||
|
this.mesh = new THREE.Mesh(geometry, material)
|
||||||
|
this.scene.add(this.mesh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update (cameraPosition: THREE.Vector3, newViewDistance: number) {
|
||||||
|
if (newViewDistance !== this.viewDistance) {
|
||||||
|
this.viewDistance = newViewDistance
|
||||||
|
this.updateSkyColors()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.mesh) {
|
||||||
|
// Update skybox position
|
||||||
|
this.mesh.position.copy(cameraPosition)
|
||||||
|
} else if (this.skyMesh) {
|
||||||
|
// Update gradient sky position
|
||||||
|
this.skyMesh.position.copy(cameraPosition)
|
||||||
|
this.voidMesh?.position.copy(cameraPosition)
|
||||||
|
this.updateSkyColors() // Update colors based on time of day
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update world time
|
||||||
|
updateTime (timeOfDay: number, partialTicks = 0) {
|
||||||
|
if (this.debugGui.visible) return
|
||||||
|
this.worldTime = timeOfDay
|
||||||
|
this.partialTicks = partialTicks
|
||||||
|
this.updateSkyColors()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update view distance
|
||||||
|
updateViewDistance (viewDistance: number) {
|
||||||
|
this.viewDistance = viewDistance
|
||||||
|
this.updateSkyColors()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update temperature (for biome support)
|
||||||
|
updateTemperature (temperature: number) {
|
||||||
|
if (this.debugGui.visible) return
|
||||||
|
this.temperature = temperature
|
||||||
|
this.updateSkyColors()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update water state
|
||||||
|
updateWaterState (inWater: boolean, waterBreathing: boolean) {
|
||||||
|
if (this.debugGui.visible) return
|
||||||
|
this.inWater = inWater
|
||||||
|
this.waterBreathing = waterBreathing
|
||||||
|
this.updateSkyColors()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update default skybox setting
|
||||||
|
updateDefaultSkybox (defaultSkybox: boolean) {
|
||||||
|
if (this.debugGui.visible) return
|
||||||
|
this.defaultSkybox = defaultSkybox
|
||||||
|
this.updateSkyColors()
|
||||||
|
}
|
||||||
|
|
||||||
|
private createGradientSky () {
|
||||||
|
const size = 64
|
||||||
|
const scale = 256 / size + 2
|
||||||
|
|
||||||
|
{
|
||||||
|
const geometry = new THREE.PlaneGeometry(size * scale * 2, size * scale * 2)
|
||||||
|
geometry.rotateX(-Math.PI / 2)
|
||||||
|
geometry.translate(0, 16, 0)
|
||||||
|
|
||||||
|
const material = new THREE.MeshBasicMaterial({
|
||||||
|
color: 0xff_ff_ff,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
depthTest: false
|
||||||
|
})
|
||||||
|
|
||||||
|
this.skyMesh = new THREE.Mesh(geometry, material)
|
||||||
|
this.scene.add(this.skyMesh)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const geometry = new THREE.PlaneGeometry(size * scale * 2, size * scale * 2)
|
||||||
|
geometry.rotateX(-Math.PI / 2)
|
||||||
|
geometry.translate(0, -16, 0)
|
||||||
|
|
||||||
|
const material = new THREE.MeshBasicMaterial({
|
||||||
|
color: 0xff_ff_ff,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
depthTest: false
|
||||||
|
})
|
||||||
|
|
||||||
|
this.voidMesh = new THREE.Mesh(geometry, material)
|
||||||
|
this.scene.add(this.voidMesh)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateSkyColors()
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFogColor (partialTicks = 0): THREE.Vector3 {
|
||||||
|
const angle = this.getCelestialAngle(partialTicks)
|
||||||
|
let rotation = Math.cos(angle * Math.PI * 2) * 2 + 0.5
|
||||||
|
rotation = Math.max(0, Math.min(1, rotation))
|
||||||
|
|
||||||
|
let x = 0.752_941_2
|
||||||
|
let y = 0.847_058_83
|
||||||
|
let z = 1
|
||||||
|
|
||||||
|
x *= (rotation * 0.94 + 0.06)
|
||||||
|
y *= (rotation * 0.94 + 0.06)
|
||||||
|
z *= (rotation * 0.91 + 0.09)
|
||||||
|
|
||||||
|
return new THREE.Vector3(x, y, z)
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSkyColor (x = 0, z = 0, partialTicks = 0): THREE.Vector3 {
|
||||||
|
const angle = this.getCelestialAngle(partialTicks)
|
||||||
|
let brightness = Math.cos(angle * 3.141_593 * 2) * 2 + 0.5
|
||||||
|
|
||||||
|
if (brightness < 0) brightness = 0
|
||||||
|
if (brightness > 1) brightness = 1
|
||||||
|
|
||||||
|
const temperature = this.getTemperature(x, z)
|
||||||
|
const rgb = this.getSkyColorByTemp(temperature)
|
||||||
|
|
||||||
|
const red = ((rgb >> 16) & 0xff) / 255
|
||||||
|
const green = ((rgb >> 8) & 0xff) / 255
|
||||||
|
const blue = (rgb & 0xff) / 255
|
||||||
|
|
||||||
|
return new THREE.Vector3(
|
||||||
|
red * brightness,
|
||||||
|
green * brightness,
|
||||||
|
blue * brightness
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateCelestialAngle (time: number, partialTicks: number): number {
|
||||||
|
const modTime = (time % 24_000)
|
||||||
|
let angle = (modTime + partialTicks) / 24_000 - 0.25
|
||||||
|
|
||||||
|
if (angle < 0) {
|
||||||
|
angle++
|
||||||
|
}
|
||||||
|
if (angle > 1) {
|
||||||
|
angle--
|
||||||
|
}
|
||||||
|
|
||||||
|
angle = 1 - ((Math.cos(angle * Math.PI) + 1) / 2)
|
||||||
|
angle += (angle - angle) / 3
|
||||||
|
|
||||||
|
return angle
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCelestialAngle (partialTicks: number): number {
|
||||||
|
return this.calculateCelestialAngle(this.worldTime, partialTicks)
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTemperature (x: number, z: number): number {
|
||||||
|
return this.temperature
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSkyColorByTemp (temperature: number): number {
|
||||||
|
temperature /= 3
|
||||||
|
if (temperature < -1) temperature = -1
|
||||||
|
if (temperature > 1) temperature = 1
|
||||||
|
|
||||||
|
// Apply debug fog orangeness to hue - positive values make it more orange, negative make it less orange
|
||||||
|
const baseHue = 0.622_222_2 - temperature * 0.05
|
||||||
|
// Orange is around hue 0.08-0.15, so we need to shift from blue-purple (0.62) toward orange
|
||||||
|
// Use a more dramatic shift and also increase saturation for more noticeable effect
|
||||||
|
const orangeHue = 0.12 // Orange hue value
|
||||||
|
const hue = this.fogOrangeness > 0
|
||||||
|
? baseHue + (orangeHue - baseHue) * this.fogOrangeness * 0.8 // Blend toward orange
|
||||||
|
: baseHue + this.fogOrangeness * 0.1 // Subtle shift for negative values
|
||||||
|
const saturation = 0.5 + temperature * 0.1 + Math.abs(this.fogOrangeness) * 0.3 // Increase saturation with orangeness
|
||||||
|
const brightness = 1
|
||||||
|
|
||||||
|
return this.hsbToRgb(hue, saturation, brightness)
|
||||||
|
}
|
||||||
|
|
||||||
|
private hsbToRgb (hue: number, saturation: number, brightness: number): number {
|
||||||
|
let r = 0; let g = 0; let b = 0
|
||||||
|
if (saturation === 0) {
|
||||||
|
r = g = b = Math.floor(brightness * 255 + 0.5)
|
||||||
|
} else {
|
||||||
|
const h = (hue - Math.floor(hue)) * 6
|
||||||
|
const f = h - Math.floor(h)
|
||||||
|
const p = brightness * (1 - saturation)
|
||||||
|
const q = brightness * (1 - saturation * f)
|
||||||
|
const t = brightness * (1 - (saturation * (1 - f)))
|
||||||
|
switch (Math.floor(h)) {
|
||||||
|
case 0:
|
||||||
|
r = Math.floor(brightness * 255 + 0.5)
|
||||||
|
g = Math.floor(t * 255 + 0.5)
|
||||||
|
b = Math.floor(p * 255 + 0.5)
|
||||||
|
break
|
||||||
|
case 1:
|
||||||
|
r = Math.floor(q * 255 + 0.5)
|
||||||
|
g = Math.floor(brightness * 255 + 0.5)
|
||||||
|
b = Math.floor(p * 255 + 0.5)
|
||||||
|
break
|
||||||
|
case 2:
|
||||||
|
r = Math.floor(p * 255 + 0.5)
|
||||||
|
g = Math.floor(brightness * 255 + 0.5)
|
||||||
|
b = Math.floor(t * 255 + 0.5)
|
||||||
|
break
|
||||||
|
case 3:
|
||||||
|
r = Math.floor(p * 255 + 0.5)
|
||||||
|
g = Math.floor(q * 255 + 0.5)
|
||||||
|
b = Math.floor(brightness * 255 + 0.5)
|
||||||
|
break
|
||||||
|
case 4:
|
||||||
|
r = Math.floor(t * 255 + 0.5)
|
||||||
|
g = Math.floor(p * 255 + 0.5)
|
||||||
|
b = Math.floor(brightness * 255 + 0.5)
|
||||||
|
break
|
||||||
|
case 5:
|
||||||
|
r = Math.floor(brightness * 255 + 0.5)
|
||||||
|
g = Math.floor(p * 255 + 0.5)
|
||||||
|
b = Math.floor(q * 255 + 0.5)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0xff_00_00_00 | (r << 16) | (g << 8) | (Math.trunc(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateSkyColors () {
|
||||||
|
if (!this.skyMesh || !this.voidMesh) return
|
||||||
|
|
||||||
|
// If default skybox is disabled, hide the skybox meshes
|
||||||
|
if (!this.defaultSkybox) {
|
||||||
|
this.skyMesh.visible = false
|
||||||
|
this.voidMesh.visible = false
|
||||||
|
if (this.mesh) {
|
||||||
|
this.mesh.visible = false
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show skybox meshes when default skybox is enabled
|
||||||
|
this.skyMesh.visible = true
|
||||||
|
this.voidMesh.visible = true
|
||||||
|
if (this.mesh) {
|
||||||
|
this.mesh.visible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update fog brightness with smooth transition
|
||||||
|
this.prevFogBrightness = this.fogBrightness
|
||||||
|
const renderDistance = this.viewDistance / 32
|
||||||
|
const targetBrightness = this.brightnessAtPosition * (1 - renderDistance) + renderDistance
|
||||||
|
this.fogBrightness += (targetBrightness - this.fogBrightness) * 0.1
|
||||||
|
|
||||||
|
// Handle water fog
|
||||||
|
if (this.inWater) {
|
||||||
|
const waterViewDistance = this.waterBreathing ? 100 : 5
|
||||||
|
this.scene.fog = new THREE.Fog(new THREE.Color(0, 0, 1), 0.0025, waterViewDistance)
|
||||||
|
this.scene.background = new THREE.Color(0, 0, 1)
|
||||||
|
|
||||||
|
// Update sky and void colors for underwater effect
|
||||||
|
;(this.skyMesh.material as THREE.MeshBasicMaterial).color.set(new THREE.Color(0, 0, 1))
|
||||||
|
;(this.voidMesh.material as THREE.MeshBasicMaterial).color.set(new THREE.Color(0, 0, 0.6))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal sky colors
|
||||||
|
const viewDistance = this.viewDistance * 16
|
||||||
|
const viewFactor = 1 - (0.25 + 0.75 * this.viewDistance / 32) ** 0.25
|
||||||
|
|
||||||
|
const angle = this.getCelestialAngle(this.partialTicks)
|
||||||
|
const skyColor = this.getSkyColor(0, 0, this.partialTicks)
|
||||||
|
const fogColor = this.getFogColor(this.partialTicks)
|
||||||
|
|
||||||
|
const brightness = Math.cos(angle * Math.PI * 2) * 2 + 0.5
|
||||||
|
const clampedBrightness = Math.max(0, Math.min(1, brightness))
|
||||||
|
|
||||||
|
// Interpolate fog brightness
|
||||||
|
const interpolatedBrightness = this.prevFogBrightness + (this.fogBrightness - this.prevFogBrightness) * this.partialTicks
|
||||||
|
|
||||||
|
const red = (fogColor.x + (skyColor.x - fogColor.x) * viewFactor) * clampedBrightness * interpolatedBrightness
|
||||||
|
const green = (fogColor.y + (skyColor.y - fogColor.y) * viewFactor) * clampedBrightness * interpolatedBrightness
|
||||||
|
const blue = (fogColor.z + (skyColor.z - fogColor.z) * viewFactor) * clampedBrightness * interpolatedBrightness
|
||||||
|
|
||||||
|
this.scene.background = new THREE.Color(red, green, blue)
|
||||||
|
this.scene.fog = new THREE.Fog(new THREE.Color(red, green, blue), 0.0025, viewDistance * this.distanceFactor)
|
||||||
|
|
||||||
|
;(this.skyMesh.material as THREE.MeshBasicMaterial).color.set(new THREE.Color(skyColor.x, skyColor.y, skyColor.z))
|
||||||
|
;(this.voidMesh.material as THREE.MeshBasicMaterial).color.set(new THREE.Color(
|
||||||
|
skyColor.x * 0.2 + 0.04,
|
||||||
|
skyColor.y * 0.2 + 0.04,
|
||||||
|
skyColor.z * 0.6 + 0.1
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose () {
|
||||||
|
if (this.texture) {
|
||||||
|
this.texture.dispose()
|
||||||
|
}
|
||||||
|
if (this.mesh) {
|
||||||
|
this.mesh.geometry.dispose()
|
||||||
|
;(this.mesh.material as THREE.Material).dispose()
|
||||||
|
this.scene.remove(this.mesh)
|
||||||
|
}
|
||||||
|
if (this.skyMesh) {
|
||||||
|
this.skyMesh.geometry.dispose()
|
||||||
|
;(this.skyMesh.material as THREE.Material).dispose()
|
||||||
|
this.scene.remove(this.skyMesh)
|
||||||
|
}
|
||||||
|
if (this.voidMesh) {
|
||||||
|
this.voidMesh.geometry.dispose()
|
||||||
|
;(this.voidMesh.material as THREE.Material).dispose()
|
||||||
|
this.scene.remove(this.voidMesh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@ import * as THREE from 'three'
|
||||||
import { WorldRendererThree } from './worldrendererThree'
|
import { WorldRendererThree } from './worldrendererThree'
|
||||||
|
|
||||||
export interface SoundSystem {
|
export interface SoundSystem {
|
||||||
playSound: (position: { x: number, y: number, z: number }, path: string, volume?: number, pitch?: number) => void
|
playSound: (position: { x: number, y: number, z: number }, path: string, volume?: number, pitch?: number, timeout?: number) => void
|
||||||
destroy: () => void
|
destroy: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -10,7 +10,17 @@ export class ThreeJsSound implements SoundSystem {
|
||||||
audioListener: THREE.AudioListener | undefined
|
audioListener: THREE.AudioListener | undefined
|
||||||
private readonly activeSounds = new Set<THREE.PositionalAudio>()
|
private readonly activeSounds = new Set<THREE.PositionalAudio>()
|
||||||
private readonly audioContext: AudioContext | undefined
|
private readonly audioContext: AudioContext | undefined
|
||||||
|
private readonly soundVolumes = new Map<THREE.PositionalAudio, number>()
|
||||||
|
baseVolume = 1
|
||||||
|
|
||||||
constructor (public worldRenderer: WorldRendererThree) {
|
constructor (public worldRenderer: WorldRendererThree) {
|
||||||
|
worldRenderer.onWorldSwitched.push(() => {
|
||||||
|
this.stopAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
worldRenderer.onReactiveConfigUpdated('volume', (volume) => {
|
||||||
|
this.changeVolume(volume)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
initAudioListener () {
|
initAudioListener () {
|
||||||
|
|
@ -19,41 +29,63 @@ export class ThreeJsSound implements SoundSystem {
|
||||||
this.worldRenderer.camera.add(this.audioListener)
|
this.worldRenderer.camera.add(this.audioListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
playSound (position: { x: number, y: number, z: number }, path: string, volume = 1, pitch = 1) {
|
playSound (position: { x: number, y: number, z: number }, path: string, volume = 1, pitch = 1, timeout = 500) {
|
||||||
this.initAudioListener()
|
this.initAudioListener()
|
||||||
|
|
||||||
const sound = new THREE.PositionalAudio(this.audioListener!)
|
const sound = new THREE.PositionalAudio(this.audioListener!)
|
||||||
this.activeSounds.add(sound)
|
this.activeSounds.add(sound)
|
||||||
|
this.soundVolumes.set(sound, volume)
|
||||||
|
|
||||||
const audioLoader = new THREE.AudioLoader()
|
const audioLoader = new THREE.AudioLoader()
|
||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
void audioLoader.loadAsync(path).then((buffer) => {
|
void audioLoader.loadAsync(path).then((buffer) => {
|
||||||
if (Date.now() - start > 500) return
|
if (Date.now() - start > timeout) {
|
||||||
|
console.warn('Ignored playing sound', path, 'due to timeout:', timeout, 'ms <', Date.now() - start, 'ms')
|
||||||
|
return
|
||||||
|
}
|
||||||
// play
|
// play
|
||||||
sound.setBuffer(buffer)
|
sound.setBuffer(buffer)
|
||||||
sound.setRefDistance(20)
|
sound.setRefDistance(20)
|
||||||
sound.setVolume(volume)
|
sound.setVolume(volume * this.baseVolume)
|
||||||
sound.setPlaybackRate(pitch) // set the pitch
|
sound.setPlaybackRate(pitch) // set the pitch
|
||||||
this.worldRenderer.scene.add(sound)
|
this.worldRenderer.scene.add(sound)
|
||||||
// set sound position
|
// set sound position
|
||||||
sound.position.set(position.x, position.y, position.z)
|
sound.position.set(position.x, position.y, position.z)
|
||||||
sound.onEnded = () => {
|
sound.onEnded = () => {
|
||||||
this.worldRenderer.scene.remove(sound)
|
this.worldRenderer.scene.remove(sound)
|
||||||
sound.disconnect()
|
if (sound.source) {
|
||||||
|
sound.disconnect()
|
||||||
|
}
|
||||||
this.activeSounds.delete(sound)
|
this.activeSounds.delete(sound)
|
||||||
|
this.soundVolumes.delete(sound)
|
||||||
audioLoader.manager.itemEnd(path)
|
audioLoader.manager.itemEnd(path)
|
||||||
}
|
}
|
||||||
sound.play()
|
sound.play()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy () {
|
stopAll () {
|
||||||
// Stop and clean up all active sounds
|
|
||||||
for (const sound of this.activeSounds) {
|
for (const sound of this.activeSounds) {
|
||||||
|
if (!sound) continue
|
||||||
sound.stop()
|
sound.stop()
|
||||||
sound.disconnect()
|
if (sound.source) {
|
||||||
|
sound.disconnect()
|
||||||
|
}
|
||||||
|
this.worldRenderer.scene.remove(sound)
|
||||||
}
|
}
|
||||||
|
this.activeSounds.clear()
|
||||||
|
this.soundVolumes.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
changeVolume (volume: number) {
|
||||||
|
this.baseVolume = volume
|
||||||
|
for (const [sound, individualVolume] of this.soundVolumes) {
|
||||||
|
sound.setVolume(individualVolume * this.baseVolume)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy () {
|
||||||
|
this.stopAll()
|
||||||
// Remove and cleanup audio listener
|
// Remove and cleanup audio listener
|
||||||
if (this.audioListener) {
|
if (this.audioListener) {
|
||||||
this.audioListener.removeFromParent()
|
this.audioListener.removeFromParent()
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
|
import { getLoadedImage } from 'mc-assets/dist/utils'
|
||||||
|
import { createCanvas } from '../lib/utils'
|
||||||
|
|
||||||
export const disposeObject = (obj: THREE.Object3D, cleanTextures = false) => {
|
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
|
// not cleaning texture there as it might be used by other objects, but would be good to also do that
|
||||||
|
|
@ -16,3 +18,56 @@ export const disposeObject = (obj: THREE.Object3D, cleanTextures = false) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let textureCache: Record<string, THREE.Texture> = {}
|
||||||
|
let imagesPromises: Record<string, Promise<THREE.Texture>> = {}
|
||||||
|
|
||||||
|
export const loadThreeJsTextureFromUrlSync = (imageUrl: string) => {
|
||||||
|
const texture = new THREE.Texture()
|
||||||
|
const promise = getLoadedImage(imageUrl).then(image => {
|
||||||
|
texture.image = image
|
||||||
|
texture.needsUpdate = true
|
||||||
|
return texture
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
texture,
|
||||||
|
promise
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadThreeJsTextureFromUrl = async (imageUrl: string) => {
|
||||||
|
const loaded = new THREE.TextureLoader().loadAsync(imageUrl)
|
||||||
|
return loaded
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadThreeJsTextureFromBitmap = (image: ImageBitmap) => {
|
||||||
|
const canvas = createCanvas(image.width, image.height)
|
||||||
|
const ctx = canvas.getContext('2d')!
|
||||||
|
ctx.drawImage(image, 0, 0)
|
||||||
|
const texture = new THREE.Texture(canvas)
|
||||||
|
texture.magFilter = THREE.NearestFilter
|
||||||
|
texture.minFilter = THREE.NearestFilter
|
||||||
|
return 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>()
|
||||||
|
const t = loadThreeJsTextureFromUrlSync(texture)
|
||||||
|
textureCache[texture] = t.texture
|
||||||
|
void t.promise.then(resolve)
|
||||||
|
imagesPromises[texture] = promise
|
||||||
|
}
|
||||||
|
|
||||||
|
cb(textureCache[texture])
|
||||||
|
void imagesPromises[texture].then(() => {
|
||||||
|
onLoad?.()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const clearTextureCache = () => {
|
||||||
|
textureCache = {}
|
||||||
|
imagesPromises = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
418
renderer/viewer/three/waypointSprite.ts
Normal file
418
renderer/viewer/three/waypointSprite.ts
Normal file
|
|
@ -0,0 +1,418 @@
|
||||||
|
import * as THREE from 'three'
|
||||||
|
|
||||||
|
// Centralized visual configuration (in screen pixels)
|
||||||
|
export const WAYPOINT_CONFIG = {
|
||||||
|
// Target size in screen pixels (this controls the final sprite size)
|
||||||
|
TARGET_SCREEN_PX: 150,
|
||||||
|
// Canvas size for internal rendering (keep power of 2 for textures)
|
||||||
|
CANVAS_SIZE: 256,
|
||||||
|
// Relative positions in canvas (0-1)
|
||||||
|
LAYOUT: {
|
||||||
|
DOT_Y: 0.3,
|
||||||
|
NAME_Y: 0.45,
|
||||||
|
DISTANCE_Y: 0.55,
|
||||||
|
},
|
||||||
|
// Multiplier for canvas internal resolution to keep text crisp
|
||||||
|
CANVAS_SCALE: 2,
|
||||||
|
ARROW: {
|
||||||
|
enabledDefault: false,
|
||||||
|
pixelSize: 50,
|
||||||
|
paddingPx: 50,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WaypointSprite = {
|
||||||
|
group: THREE.Group
|
||||||
|
sprite: THREE.Sprite
|
||||||
|
// Offscreen arrow controls
|
||||||
|
enableOffscreenArrow: (enabled: boolean) => void
|
||||||
|
setArrowParent: (parent: THREE.Object3D | null) => void
|
||||||
|
// Convenience combined updater
|
||||||
|
updateForCamera: (
|
||||||
|
cameraPosition: THREE.Vector3,
|
||||||
|
camera: THREE.PerspectiveCamera,
|
||||||
|
viewportWidthPx: number,
|
||||||
|
viewportHeightPx: number
|
||||||
|
) => boolean
|
||||||
|
// Utilities
|
||||||
|
setColor: (color: number) => void
|
||||||
|
setLabel: (label?: string) => void
|
||||||
|
updateDistanceText: (label: string, distanceText: string) => void
|
||||||
|
setVisible: (visible: boolean) => void
|
||||||
|
setPosition: (x: number, y: number, z: number) => void
|
||||||
|
dispose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createWaypointSprite (options: {
|
||||||
|
position: THREE.Vector3 | { x: number, y: number, z: number },
|
||||||
|
color?: number,
|
||||||
|
label?: string,
|
||||||
|
depthTest?: boolean,
|
||||||
|
// Y offset in world units used by updateScaleWorld only (screen-pixel API ignores this)
|
||||||
|
labelYOffset?: number,
|
||||||
|
metadata?: any,
|
||||||
|
}): WaypointSprite {
|
||||||
|
const color = options.color ?? 0xFF_00_00
|
||||||
|
const depthTest = options.depthTest ?? false
|
||||||
|
const labelYOffset = options.labelYOffset ?? 1.5
|
||||||
|
|
||||||
|
// Build combined sprite
|
||||||
|
const sprite = createCombinedSprite(color, options.label ?? '', '0m', depthTest)
|
||||||
|
sprite.renderOrder = 10
|
||||||
|
let currentLabel = options.label ?? ''
|
||||||
|
|
||||||
|
// Offscreen arrow (detached by default)
|
||||||
|
let arrowSprite: THREE.Sprite | undefined
|
||||||
|
let arrowParent: THREE.Object3D | null = null
|
||||||
|
let arrowEnabled = WAYPOINT_CONFIG.ARROW.enabledDefault
|
||||||
|
|
||||||
|
// Group for easy add/remove
|
||||||
|
const group = new THREE.Group()
|
||||||
|
group.add(sprite)
|
||||||
|
|
||||||
|
// Initial position
|
||||||
|
const { x, y, z } = options.position
|
||||||
|
group.position.set(x, y, z)
|
||||||
|
|
||||||
|
function setColor (newColor: number) {
|
||||||
|
const canvas = drawCombinedCanvas(newColor, currentLabel, '0m')
|
||||||
|
const texture = new THREE.CanvasTexture(canvas)
|
||||||
|
const mat = sprite.material
|
||||||
|
mat.map?.dispose()
|
||||||
|
mat.map = texture
|
||||||
|
mat.needsUpdate = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLabel (newLabel?: string) {
|
||||||
|
currentLabel = newLabel ?? ''
|
||||||
|
const canvas = drawCombinedCanvas(color, currentLabel, '0m')
|
||||||
|
const texture = new THREE.CanvasTexture(canvas)
|
||||||
|
const mat = sprite.material
|
||||||
|
mat.map?.dispose()
|
||||||
|
mat.map = texture
|
||||||
|
mat.needsUpdate = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDistanceText (label: string, distanceText: string) {
|
||||||
|
const canvas = drawCombinedCanvas(color, label, distanceText)
|
||||||
|
const texture = new THREE.CanvasTexture(canvas)
|
||||||
|
const mat = sprite.material
|
||||||
|
mat.map?.dispose()
|
||||||
|
mat.map = texture
|
||||||
|
mat.needsUpdate = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function setVisible (visible: boolean) {
|
||||||
|
sprite.visible = visible
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPosition (nx: number, ny: number, nz: number) {
|
||||||
|
group.position.set(nx, ny, nz)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep constant pixel size on screen using global config
|
||||||
|
function updateScaleScreenPixels (
|
||||||
|
cameraPosition: THREE.Vector3,
|
||||||
|
cameraFov: number,
|
||||||
|
distance: number,
|
||||||
|
viewportHeightPx: number
|
||||||
|
) {
|
||||||
|
const vFovRad = cameraFov * Math.PI / 180
|
||||||
|
const worldUnitsPerScreenHeightAtDist = Math.tan(vFovRad / 2) * 2 * distance
|
||||||
|
// Use configured target screen size
|
||||||
|
const scale = worldUnitsPerScreenHeightAtDist * (WAYPOINT_CONFIG.TARGET_SCREEN_PX / viewportHeightPx)
|
||||||
|
sprite.scale.set(scale, scale, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureArrow () {
|
||||||
|
if (arrowSprite) return
|
||||||
|
const size = 128
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = size
|
||||||
|
canvas.height = size
|
||||||
|
const ctx = canvas.getContext('2d')!
|
||||||
|
ctx.clearRect(0, 0, size, size)
|
||||||
|
|
||||||
|
// Draw arrow shape
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(size * 0.15, size * 0.5)
|
||||||
|
ctx.lineTo(size * 0.85, size * 0.5)
|
||||||
|
ctx.lineTo(size * 0.5, size * 0.15)
|
||||||
|
ctx.closePath()
|
||||||
|
|
||||||
|
// Use waypoint color for arrow
|
||||||
|
const colorHex = `#${color.toString(16).padStart(6, '0')}`
|
||||||
|
ctx.lineWidth = 6
|
||||||
|
ctx.strokeStyle = 'black'
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.fillStyle = colorHex
|
||||||
|
ctx.fill()
|
||||||
|
|
||||||
|
const texture = new THREE.CanvasTexture(canvas)
|
||||||
|
const material = new THREE.SpriteMaterial({ map: texture, transparent: true, depthTest: false, depthWrite: false })
|
||||||
|
arrowSprite = new THREE.Sprite(material)
|
||||||
|
arrowSprite.renderOrder = 12
|
||||||
|
arrowSprite.visible = false
|
||||||
|
if (arrowParent) arrowParent.add(arrowSprite)
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableOffscreenArrow (enabled: boolean) {
|
||||||
|
arrowEnabled = enabled
|
||||||
|
if (!enabled && arrowSprite) arrowSprite.visible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function setArrowParent (parent: THREE.Object3D | null) {
|
||||||
|
if (arrowSprite?.parent) arrowSprite.parent.remove(arrowSprite)
|
||||||
|
arrowParent = parent
|
||||||
|
if (arrowSprite && parent) parent.add(arrowSprite)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateOffscreenArrow (
|
||||||
|
camera: THREE.PerspectiveCamera,
|
||||||
|
viewportWidthPx: number,
|
||||||
|
viewportHeightPx: number
|
||||||
|
): boolean {
|
||||||
|
if (!arrowEnabled) return true
|
||||||
|
ensureArrow()
|
||||||
|
if (!arrowSprite) return true
|
||||||
|
|
||||||
|
// Check if onlyLeftRight is enabled in metadata
|
||||||
|
const onlyLeftRight = options.metadata?.onlyLeftRight === true
|
||||||
|
|
||||||
|
// Build camera basis using camera.up to respect custom orientations
|
||||||
|
const forward = new THREE.Vector3()
|
||||||
|
camera.getWorldDirection(forward) // camera look direction
|
||||||
|
const upWorld = camera.up.clone().normalize()
|
||||||
|
const right = new THREE.Vector3().copy(forward).cross(upWorld).normalize()
|
||||||
|
const upCam = new THREE.Vector3().copy(right).cross(forward).normalize()
|
||||||
|
|
||||||
|
// Vector from camera to waypoint
|
||||||
|
const camPos = new THREE.Vector3().setFromMatrixPosition(camera.matrixWorld)
|
||||||
|
const toWp = new THREE.Vector3(group.position.x, group.position.y, group.position.z).sub(camPos)
|
||||||
|
|
||||||
|
// Components in camera basis
|
||||||
|
const z = toWp.dot(forward)
|
||||||
|
const x = toWp.dot(right)
|
||||||
|
const y = toWp.dot(upCam)
|
||||||
|
|
||||||
|
const aspect = viewportWidthPx / viewportHeightPx
|
||||||
|
const vFovRad = camera.fov * Math.PI / 180
|
||||||
|
const hFovRad = 2 * Math.atan(Math.tan(vFovRad / 2) * aspect)
|
||||||
|
|
||||||
|
// Determine if waypoint is inside view frustum using angular checks
|
||||||
|
const thetaX = Math.atan2(x, z)
|
||||||
|
const thetaY = Math.atan2(y, z)
|
||||||
|
const visible = z > 0 && Math.abs(thetaX) <= hFovRad / 2 && Math.abs(thetaY) <= vFovRad / 2
|
||||||
|
if (visible) {
|
||||||
|
arrowSprite.visible = false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direction on screen in normalized frustum units
|
||||||
|
let rx = thetaX / (hFovRad / 2)
|
||||||
|
let ry = thetaY / (vFovRad / 2)
|
||||||
|
|
||||||
|
// If behind the camera, snap to dominant axis to avoid confusing directions
|
||||||
|
if (z <= 0) {
|
||||||
|
if (Math.abs(rx) > Math.abs(ry)) {
|
||||||
|
rx = Math.sign(rx)
|
||||||
|
ry = 0
|
||||||
|
} else {
|
||||||
|
rx = 0
|
||||||
|
ry = Math.sign(ry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply onlyLeftRight logic - restrict arrows to left/right edges only
|
||||||
|
if (onlyLeftRight) {
|
||||||
|
// Force the arrow to appear only on left or right edges
|
||||||
|
if (Math.abs(rx) > Math.abs(ry)) {
|
||||||
|
// Horizontal direction is dominant, keep it
|
||||||
|
ry = 0
|
||||||
|
} else {
|
||||||
|
// Vertical direction is dominant, but we want only left/right
|
||||||
|
// So choose left or right based on the sign of rx
|
||||||
|
rx = rx >= 0 ? 1 : -1
|
||||||
|
ry = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place on the rectangle border [-1,1]x[-1,1]
|
||||||
|
const s = Math.max(Math.abs(rx), Math.abs(ry)) || 1
|
||||||
|
let ndcX = rx / s
|
||||||
|
let ndcY = ry / s
|
||||||
|
|
||||||
|
// Apply padding in pixel space by clamping
|
||||||
|
const padding = WAYPOINT_CONFIG.ARROW.paddingPx
|
||||||
|
const pxX = ((ndcX + 1) * 0.5) * viewportWidthPx
|
||||||
|
const pxY = ((1 - ndcY) * 0.5) * viewportHeightPx
|
||||||
|
const clampedPxX = Math.min(Math.max(pxX, padding), viewportWidthPx - padding)
|
||||||
|
const clampedPxY = Math.min(Math.max(pxY, padding), viewportHeightPx - padding)
|
||||||
|
ndcX = (clampedPxX / viewportWidthPx) * 2 - 1
|
||||||
|
ndcY = -(clampedPxY / viewportHeightPx) * 2 + 1
|
||||||
|
|
||||||
|
// Compute world position at a fixed distance in front of the camera using camera basis
|
||||||
|
const placeDist = Math.max(2, camera.near * 4)
|
||||||
|
const halfPlaneHeight = Math.tan(vFovRad / 2) * placeDist
|
||||||
|
const halfPlaneWidth = halfPlaneHeight * aspect
|
||||||
|
const pos = camPos.clone()
|
||||||
|
.add(forward.clone().multiplyScalar(placeDist))
|
||||||
|
.add(right.clone().multiplyScalar(ndcX * halfPlaneWidth))
|
||||||
|
.add(upCam.clone().multiplyScalar(ndcY * halfPlaneHeight))
|
||||||
|
|
||||||
|
// Update arrow sprite
|
||||||
|
arrowSprite.visible = true
|
||||||
|
arrowSprite.position.copy(pos)
|
||||||
|
|
||||||
|
// Angle for rotation relative to screen right/up (derived from camera up vector)
|
||||||
|
const angle = Math.atan2(ry, rx)
|
||||||
|
arrowSprite.material.rotation = angle - Math.PI / 2
|
||||||
|
|
||||||
|
// Constant pixel size for arrow (use fixed placement distance)
|
||||||
|
const worldUnitsPerScreenHeightAtDist = Math.tan(vFovRad / 2) * 2 * placeDist
|
||||||
|
const sPx = worldUnitsPerScreenHeightAtDist * (WAYPOINT_CONFIG.ARROW.pixelSize / viewportHeightPx)
|
||||||
|
arrowSprite.scale.set(sPx, sPx, 1)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeDistance (cameraPosition: THREE.Vector3): number {
|
||||||
|
return cameraPosition.distanceTo(group.position)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateForCamera (
|
||||||
|
cameraPosition: THREE.Vector3,
|
||||||
|
camera: THREE.PerspectiveCamera,
|
||||||
|
viewportWidthPx: number,
|
||||||
|
viewportHeightPx: number
|
||||||
|
): boolean {
|
||||||
|
const distance = computeDistance(cameraPosition)
|
||||||
|
// Keep constant pixel size
|
||||||
|
updateScaleScreenPixels(cameraPosition, camera.fov, distance, viewportHeightPx)
|
||||||
|
// Update text
|
||||||
|
updateDistanceText(currentLabel, `${Math.round(distance)}m`)
|
||||||
|
// Update arrow and visibility
|
||||||
|
const onScreen = updateOffscreenArrow(camera, viewportWidthPx, viewportHeightPx)
|
||||||
|
setVisible(onScreen)
|
||||||
|
return onScreen
|
||||||
|
}
|
||||||
|
|
||||||
|
function dispose () {
|
||||||
|
const mat = sprite.material
|
||||||
|
mat.map?.dispose()
|
||||||
|
mat.dispose()
|
||||||
|
if (arrowSprite) {
|
||||||
|
const am = arrowSprite.material
|
||||||
|
am.map?.dispose()
|
||||||
|
am.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
group,
|
||||||
|
sprite,
|
||||||
|
enableOffscreenArrow,
|
||||||
|
setArrowParent,
|
||||||
|
updateForCamera,
|
||||||
|
setColor,
|
||||||
|
setLabel,
|
||||||
|
updateDistanceText,
|
||||||
|
setVisible,
|
||||||
|
setPosition,
|
||||||
|
dispose,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal helpers
|
||||||
|
function drawCombinedCanvas (color: number, id: string, distance: string): HTMLCanvasElement {
|
||||||
|
const scale = WAYPOINT_CONFIG.CANVAS_SCALE * (globalThis.devicePixelRatio || 1)
|
||||||
|
const size = WAYPOINT_CONFIG.CANVAS_SIZE * scale
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = size
|
||||||
|
canvas.height = size
|
||||||
|
const ctx = canvas.getContext('2d')!
|
||||||
|
|
||||||
|
// Clear canvas
|
||||||
|
ctx.clearRect(0, 0, size, size)
|
||||||
|
|
||||||
|
// Draw dot
|
||||||
|
const centerX = size / 2
|
||||||
|
const dotY = Math.round(size * WAYPOINT_CONFIG.LAYOUT.DOT_Y)
|
||||||
|
const radius = Math.round(size * 0.05) // Dot takes up ~12% of canvas height
|
||||||
|
const borderWidth = Math.max(2, Math.round(4 * scale))
|
||||||
|
|
||||||
|
// Outer border (black)
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(centerX, dotY, radius + borderWidth, 0, Math.PI * 2)
|
||||||
|
ctx.fillStyle = 'black'
|
||||||
|
ctx.fill()
|
||||||
|
|
||||||
|
// Inner circle (colored)
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(centerX, dotY, radius, 0, Math.PI * 2)
|
||||||
|
ctx.fillStyle = `#${color.toString(16).padStart(6, '0')}`
|
||||||
|
ctx.fill()
|
||||||
|
|
||||||
|
// Text properties
|
||||||
|
ctx.textAlign = 'center'
|
||||||
|
ctx.textBaseline = 'middle'
|
||||||
|
|
||||||
|
// Title
|
||||||
|
const nameFontPx = Math.round(size * 0.08) // ~8% of canvas height
|
||||||
|
const distanceFontPx = Math.round(size * 0.06) // ~6% of canvas height
|
||||||
|
ctx.font = `bold ${nameFontPx}px mojangles`
|
||||||
|
ctx.lineWidth = Math.max(2, Math.round(3 * scale))
|
||||||
|
const nameY = Math.round(size * WAYPOINT_CONFIG.LAYOUT.NAME_Y)
|
||||||
|
|
||||||
|
ctx.strokeStyle = 'black'
|
||||||
|
ctx.strokeText(id, centerX, nameY)
|
||||||
|
ctx.fillStyle = 'white'
|
||||||
|
ctx.fillText(id, centerX, nameY)
|
||||||
|
|
||||||
|
// Distance
|
||||||
|
ctx.font = `bold ${distanceFontPx}px mojangles`
|
||||||
|
ctx.lineWidth = Math.max(2, Math.round(2 * scale))
|
||||||
|
const distanceY = Math.round(size * WAYPOINT_CONFIG.LAYOUT.DISTANCE_Y)
|
||||||
|
|
||||||
|
ctx.strokeStyle = 'black'
|
||||||
|
ctx.strokeText(distance, centerX, distanceY)
|
||||||
|
ctx.fillStyle = '#CCCCCC'
|
||||||
|
ctx.fillText(distance, centerX, distanceY)
|
||||||
|
|
||||||
|
return canvas
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCombinedSprite (color: number, id: string, distance: string, depthTest: boolean): THREE.Sprite {
|
||||||
|
const canvas = drawCombinedCanvas(color, id, distance)
|
||||||
|
const texture = new THREE.CanvasTexture(canvas)
|
||||||
|
texture.anisotropy = 1
|
||||||
|
texture.magFilter = THREE.LinearFilter
|
||||||
|
texture.minFilter = THREE.LinearFilter
|
||||||
|
const material = new THREE.SpriteMaterial({
|
||||||
|
map: texture,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 1,
|
||||||
|
depthTest,
|
||||||
|
depthWrite: false,
|
||||||
|
})
|
||||||
|
const sprite = new THREE.Sprite(material)
|
||||||
|
sprite.position.set(0, 0, 0)
|
||||||
|
return sprite
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WaypointHelpers = {
|
||||||
|
// World-scale constant size helper
|
||||||
|
computeWorldScale (distance: number, fixedReference = 10) {
|
||||||
|
return Math.max(0.0001, distance / fixedReference)
|
||||||
|
},
|
||||||
|
// Screen-pixel constant size helper
|
||||||
|
computeScreenPixelScale (
|
||||||
|
camera: THREE.PerspectiveCamera,
|
||||||
|
distance: number,
|
||||||
|
pixelSize: number,
|
||||||
|
viewportHeightPx: number
|
||||||
|
) {
|
||||||
|
const vFovRad = camera.fov * Math.PI / 180
|
||||||
|
const worldUnitsPerScreenHeightAtDist = Math.tan(vFovRad / 2) * 2 * distance
|
||||||
|
return worldUnitsPerScreenHeightAtDist * (pixelSize / viewportHeightPx)
|
||||||
|
}
|
||||||
|
}
|
||||||
140
renderer/viewer/three/waypoints.ts
Normal file
140
renderer/viewer/three/waypoints.ts
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
import * as THREE from 'three'
|
||||||
|
import { WorldRendererThree } from './worldrendererThree'
|
||||||
|
import { createWaypointSprite, type WaypointSprite } from './waypointSprite'
|
||||||
|
|
||||||
|
interface Waypoint {
|
||||||
|
id: string
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
z: number
|
||||||
|
minDistance: number
|
||||||
|
color: number
|
||||||
|
label?: string
|
||||||
|
sprite: WaypointSprite
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WaypointOptions {
|
||||||
|
color?: number
|
||||||
|
label?: string
|
||||||
|
minDistance?: number
|
||||||
|
metadata?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WaypointsRenderer {
|
||||||
|
private readonly waypoints = new Map<string, Waypoint>()
|
||||||
|
private readonly waypointScene = new THREE.Scene()
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private readonly worldRenderer: WorldRendererThree
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateWaypoints () {
|
||||||
|
const playerPos = this.worldRenderer.cameraObject.position
|
||||||
|
const sizeVec = this.worldRenderer.renderer.getSize(new THREE.Vector2())
|
||||||
|
|
||||||
|
for (const waypoint of this.waypoints.values()) {
|
||||||
|
const waypointPos = new THREE.Vector3(waypoint.x, waypoint.y, waypoint.z)
|
||||||
|
const distance = playerPos.distanceTo(waypointPos)
|
||||||
|
const visible = !waypoint.minDistance || distance >= waypoint.minDistance
|
||||||
|
|
||||||
|
waypoint.sprite.setVisible(visible)
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
// Update position
|
||||||
|
waypoint.sprite.setPosition(waypoint.x, waypoint.y, waypoint.z)
|
||||||
|
// Ensure camera-based update each frame
|
||||||
|
waypoint.sprite.updateForCamera(this.worldRenderer.getCameraPosition(), this.worldRenderer.camera, sizeVec.width, sizeVec.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
if (this.waypoints.size === 0) return
|
||||||
|
|
||||||
|
// Update waypoint scaling
|
||||||
|
this.updateWaypoints()
|
||||||
|
|
||||||
|
// Render waypoints scene with the world camera
|
||||||
|
this.worldRenderer.renderer.render(this.waypointScene, this.worldRenderer.camera)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removed sprite/label texture creation. Use utils/waypointSprite.ts
|
||||||
|
|
||||||
|
addWaypoint (
|
||||||
|
id: string,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
z: number,
|
||||||
|
options: WaypointOptions = {}
|
||||||
|
) {
|
||||||
|
// Remove existing waypoint if it exists
|
||||||
|
this.removeWaypoint(id)
|
||||||
|
|
||||||
|
const color = options.color ?? 0xFF_00_00
|
||||||
|
const { label, metadata } = options
|
||||||
|
const minDistance = options.minDistance ?? 0
|
||||||
|
|
||||||
|
const sprite = createWaypointSprite({
|
||||||
|
position: new THREE.Vector3(x, y, z),
|
||||||
|
color,
|
||||||
|
label: (label || id),
|
||||||
|
metadata,
|
||||||
|
})
|
||||||
|
sprite.enableOffscreenArrow(true)
|
||||||
|
sprite.setArrowParent(this.waypointScene)
|
||||||
|
|
||||||
|
this.waypointScene.add(sprite.group)
|
||||||
|
|
||||||
|
this.waypoints.set(id, {
|
||||||
|
id, x: x + 0.5, y: y + 0.5, z: z + 0.5, minDistance,
|
||||||
|
color, label,
|
||||||
|
sprite,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
removeWaypoint (id: string) {
|
||||||
|
const waypoint = this.waypoints.get(id)
|
||||||
|
if (waypoint) {
|
||||||
|
this.waypointScene.remove(waypoint.sprite.group)
|
||||||
|
waypoint.sprite.dispose()
|
||||||
|
this.waypoints.delete(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clear () {
|
||||||
|
for (const id of this.waypoints.keys()) {
|
||||||
|
this.removeWaypoint(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testWaypoint () {
|
||||||
|
this.addWaypoint('Test Point', 0, 70, 0, { color: 0x00_FF_00, label: 'Test Point' })
|
||||||
|
this.addWaypoint('Spawn', 0, 64, 0, { color: 0xFF_FF_00, label: 'Spawn' })
|
||||||
|
this.addWaypoint('Far Point', 100, 70, 100, { color: 0x00_00_FF, label: 'Far Point' })
|
||||||
|
}
|
||||||
|
|
||||||
|
getWaypoint (id: string): Waypoint | undefined {
|
||||||
|
return this.waypoints.get(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllWaypoints (): Waypoint[] {
|
||||||
|
return [...this.waypoints.values()]
|
||||||
|
}
|
||||||
|
|
||||||
|
setWaypointColor (id: string, color: number) {
|
||||||
|
const waypoint = this.waypoints.get(id)
|
||||||
|
if (waypoint) {
|
||||||
|
waypoint.sprite.setColor(color)
|
||||||
|
waypoint.color = color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setWaypointLabel (id: string, label?: string) {
|
||||||
|
const waypoint = this.waypoints.get(id)
|
||||||
|
if (waypoint) {
|
||||||
|
waypoint.label = label
|
||||||
|
waypoint.sprite.setLabel(label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
import { LineMaterial, LineSegmentsGeometry, Wireframe } from 'three-stdlib'
|
import { LineMaterial, LineSegmentsGeometry, Wireframe } from 'three-stdlib'
|
||||||
import { Vec3 } from 'vec3'
|
import { Vec3 } from 'vec3'
|
||||||
import { subscribeKey } from 'valtio/utils'
|
|
||||||
import { Block } from 'prismarine-block'
|
|
||||||
import { BlockShape, BlocksShapes } from 'renderer/viewer/lib/basePlayerState'
|
import { BlockShape, BlocksShapes } from 'renderer/viewer/lib/basePlayerState'
|
||||||
import { WorldRendererThree } from '../worldrendererThree'
|
import { WorldRendererThree } from '../worldrendererThree'
|
||||||
|
import { loadThreeJsTextureFromUrl } from '../threeJsUtils'
|
||||||
import destroyStage0 from '../../../../assets/destroy_stage_0.png'
|
import destroyStage0 from '../../../../assets/destroy_stage_0.png'
|
||||||
import destroyStage1 from '../../../../assets/destroy_stage_1.png'
|
import destroyStage1 from '../../../../assets/destroy_stage_1.png'
|
||||||
import destroyStage2 from '../../../../assets/destroy_stage_2.png'
|
import destroyStage2 from '../../../../assets/destroy_stage_2.png'
|
||||||
|
|
@ -29,24 +28,24 @@ export class CursorBlock {
|
||||||
}
|
}
|
||||||
|
|
||||||
cursorLineMaterial: LineMaterial
|
cursorLineMaterial: LineMaterial
|
||||||
interactionLines: null | { blockPos: Vec3, mesh: THREE.Group } = null
|
interactionLines: null | { blockPos: Vec3, mesh: THREE.Group, shapePositions: BlocksShapes | undefined } = null
|
||||||
prevColor: string | undefined
|
prevColor: string | undefined
|
||||||
blockBreakMesh: THREE.Mesh
|
blockBreakMesh: THREE.Mesh
|
||||||
breakTextures: THREE.Texture[] = []
|
breakTextures: THREE.Texture[] = []
|
||||||
|
|
||||||
constructor (public readonly worldRenderer: WorldRendererThree) {
|
constructor (public readonly worldRenderer: WorldRendererThree) {
|
||||||
// Initialize break mesh and textures
|
// Initialize break mesh and textures
|
||||||
const loader = new THREE.TextureLoader()
|
|
||||||
const destroyStagesImages = [
|
const destroyStagesImages = [
|
||||||
destroyStage0, destroyStage1, destroyStage2, destroyStage3, destroyStage4,
|
destroyStage0, destroyStage1, destroyStage2, destroyStage3, destroyStage4,
|
||||||
destroyStage5, destroyStage6, destroyStage7, destroyStage8, destroyStage9
|
destroyStage5, destroyStage6, destroyStage7, destroyStage8, destroyStage9
|
||||||
]
|
]
|
||||||
|
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
const texture = loader.load(destroyStagesImages[i])
|
void loadThreeJsTextureFromUrl(destroyStagesImages[i]).then((texture) => {
|
||||||
texture.magFilter = THREE.NearestFilter
|
texture.magFilter = THREE.NearestFilter
|
||||||
texture.minFilter = THREE.NearestFilter
|
texture.minFilter = THREE.NearestFilter
|
||||||
this.breakTextures.push(texture)
|
this.breakTextures.push(texture)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const breakMaterial = new THREE.MeshBasicMaterial({
|
const breakMaterial = new THREE.MeshBasicMaterial({
|
||||||
|
|
@ -60,18 +59,26 @@ export class CursorBlock {
|
||||||
this.blockBreakMesh.name = 'blockBreakMesh'
|
this.blockBreakMesh.name = 'blockBreakMesh'
|
||||||
this.worldRenderer.scene.add(this.blockBreakMesh)
|
this.worldRenderer.scene.add(this.blockBreakMesh)
|
||||||
|
|
||||||
subscribeKey(this.worldRenderer.playerState.reactive, 'gameMode', () => {
|
this.worldRenderer.onReactivePlayerStateUpdated('gameMode', () => {
|
||||||
this.updateLineMaterial()
|
this.updateLineMaterial()
|
||||||
})
|
})
|
||||||
|
// todo figure out why otherwise fog from skybox breaks it
|
||||||
this.updateLineMaterial()
|
setTimeout(() => {
|
||||||
|
this.updateLineMaterial()
|
||||||
|
if (this.interactionLines) {
|
||||||
|
this.setHighlightCursorBlock(this.interactionLines.blockPos, this.interactionLines.shapePositions, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update functions
|
// Update functions
|
||||||
updateLineMaterial () {
|
updateLineMaterial () {
|
||||||
const inCreative = this.worldRenderer.displayOptions.playerState.reactive.gameMode === 'creative'
|
const inCreative = this.worldRenderer.playerStateReactive.gameMode === 'creative'
|
||||||
const pixelRatio = this.worldRenderer.renderer.getPixelRatio()
|
const pixelRatio = this.worldRenderer.renderer.getPixelRatio()
|
||||||
|
|
||||||
|
if (this.cursorLineMaterial) {
|
||||||
|
this.cursorLineMaterial.dispose()
|
||||||
|
}
|
||||||
this.cursorLineMaterial = new LineMaterial({
|
this.cursorLineMaterial = new LineMaterial({
|
||||||
color: (() => {
|
color: (() => {
|
||||||
switch (this.worldRenderer.worldRendererConfig.highlightBlockColor) {
|
switch (this.worldRenderer.worldRendererConfig.highlightBlockColor) {
|
||||||
|
|
@ -118,8 +125,8 @@ export class CursorBlock {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setHighlightCursorBlock (blockPos: Vec3 | null, shapePositions?: BlocksShapes): void {
|
setHighlightCursorBlock (blockPos: Vec3 | null, shapePositions?: BlocksShapes, force = false): void {
|
||||||
if (blockPos && this.interactionLines && blockPos.equals(this.interactionLines.blockPos)) {
|
if (blockPos && this.interactionLines && blockPos.equals(this.interactionLines.blockPos) && !force) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this.interactionLines !== null) {
|
if (this.interactionLines !== null) {
|
||||||
|
|
@ -143,7 +150,7 @@ export class CursorBlock {
|
||||||
}
|
}
|
||||||
this.worldRenderer.scene.add(group)
|
this.worldRenderer.scene.add(group)
|
||||||
group.visible = !this.cursorLinesHidden
|
group.visible = !this.cursorLinesHidden
|
||||||
this.interactionLines = { blockPos, mesh: group }
|
this.interactionLines = { blockPos, mesh: group, shapePositions }
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ export async function initVR (worldRenderer: WorldRendererThree, documentRendere
|
||||||
|
|
||||||
// hack for vr camera
|
// hack for vr camera
|
||||||
const user = new THREE.Group()
|
const user = new THREE.Group()
|
||||||
user.add(worldRenderer.camera)
|
user.name = 'vr-camera-container'
|
||||||
worldRenderer.scene.add(user)
|
worldRenderer.scene.add(user)
|
||||||
const controllerModelFactory = new XRControllerModelFactory(new GLTFLoader())
|
const controllerModelFactory = new XRControllerModelFactory(new GLTFLoader())
|
||||||
const controller1 = renderer.xr.getControllerGrip(0)
|
const controller1 = renderer.xr.getControllerGrip(0)
|
||||||
|
|
@ -202,10 +202,12 @@ export async function initVR (worldRenderer: WorldRendererThree, documentRendere
|
||||||
documentRenderer.frameRender(false)
|
documentRenderer.frameRender(false)
|
||||||
})
|
})
|
||||||
renderer.xr.addEventListener('sessionstart', () => {
|
renderer.xr.addEventListener('sessionstart', () => {
|
||||||
|
user.add(worldRenderer.camera)
|
||||||
worldRenderer.cameraGroupVr = user
|
worldRenderer.cameraGroupVr = user
|
||||||
})
|
})
|
||||||
renderer.xr.addEventListener('sessionend', () => {
|
renderer.xr.addEventListener('sessionend', () => {
|
||||||
worldRenderer.cameraGroupVr = undefined
|
worldRenderer.cameraGroupVr = undefined
|
||||||
|
user.remove(worldRenderer.camera)
|
||||||
})
|
})
|
||||||
|
|
||||||
worldRenderer.abortController.signal.addEventListener('abort', disableVr)
|
worldRenderer.abortController.signal.addEventListener('abort', disableVr)
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,20 @@ import { Vec3 } from 'vec3'
|
||||||
import nbt from 'prismarine-nbt'
|
import nbt from 'prismarine-nbt'
|
||||||
import PrismarineChatLoader from 'prismarine-chat'
|
import PrismarineChatLoader from 'prismarine-chat'
|
||||||
import * as tweenJs from '@tweenjs/tween.js'
|
import * as tweenJs from '@tweenjs/tween.js'
|
||||||
import { subscribeKey } from 'valtio/utils'
|
import { Biome } from 'minecraft-data'
|
||||||
import { renderSign } from '../sign-renderer'
|
import { renderSign } from '../sign-renderer'
|
||||||
import { DisplayWorldOptions, GraphicsInitOptions, RendererReactiveState } from '../../../src/appViewer'
|
import { DisplayWorldOptions, GraphicsInitOptions } from '../../../src/appViewer'
|
||||||
import { chunkPos, sectionPos } from '../lib/simpleUtils'
|
import { chunkPos, sectionPos } from '../lib/simpleUtils'
|
||||||
import { WorldRendererCommon } from '../lib/worldrendererCommon'
|
import { WorldRendererCommon } from '../lib/worldrendererCommon'
|
||||||
import { addNewStat, removeAllStats } from '../lib/ui/newStats'
|
import { addNewStat } from '../lib/ui/newStats'
|
||||||
import { MesherGeometryOutput } from '../lib/mesher/shared'
|
import { MesherGeometryOutput } from '../lib/mesher/shared'
|
||||||
import { ItemSpecificContextProperties } from '../lib/basePlayerState'
|
import { ItemSpecificContextProperties } from '../lib/basePlayerState'
|
||||||
import { getMyHand } from '../lib/hand'
|
|
||||||
import { setBlockPosition } from '../lib/mesher/standaloneRenderer'
|
import { setBlockPosition } from '../lib/mesher/standaloneRenderer'
|
||||||
import { sendVideoPlay, sendVideoStop } from '../../../src/customChannels'
|
import { getMyHand } from './hand'
|
||||||
import HoldingBlock from './holdingBlock'
|
import HoldingBlock from './holdingBlock'
|
||||||
import { getMesh } from './entity/EntityMesh'
|
import { getMesh } from './entity/EntityMesh'
|
||||||
import { armorModel } from './entity/armorModels'
|
import { armorModel } from './entity/armorModels'
|
||||||
import { disposeObject } from './threeJsUtils'
|
import { disposeObject, loadThreeJsTextureFromBitmap } from './threeJsUtils'
|
||||||
import { CursorBlock } from './world/cursorBlock'
|
import { CursorBlock } from './world/cursorBlock'
|
||||||
import { getItemUv } from './appShared'
|
import { getItemUv } from './appShared'
|
||||||
import { Entities } from './entities'
|
import { Entities } from './entities'
|
||||||
|
|
@ -25,6 +24,8 @@ import { ThreeJsSound } from './threeJsSound'
|
||||||
import { CameraShake } from './cameraShake'
|
import { CameraShake } from './cameraShake'
|
||||||
import { ThreeJsMedia } from './threeJsMedia'
|
import { ThreeJsMedia } from './threeJsMedia'
|
||||||
import { Fountain } from './threeJsParticles'
|
import { Fountain } from './threeJsParticles'
|
||||||
|
import { WaypointsRenderer } from './waypoints'
|
||||||
|
import { DEFAULT_TEMPERATURE, SkyboxRenderer } from './skyboxRenderer'
|
||||||
|
|
||||||
type SectionKey = string
|
type SectionKey = string
|
||||||
|
|
||||||
|
|
@ -44,11 +45,13 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
cameraGroupVr?: THREE.Object3D
|
cameraGroupVr?: THREE.Object3D
|
||||||
material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.1 })
|
material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.1 })
|
||||||
itemsTexture: THREE.Texture
|
itemsTexture: THREE.Texture
|
||||||
cursorBlock = new CursorBlock(this)
|
cursorBlock: CursorBlock
|
||||||
onRender: Array<() => void> = []
|
onRender: Array<() => void> = []
|
||||||
cameraShake: CameraShake
|
cameraShake: CameraShake
|
||||||
|
cameraContainer: THREE.Object3D
|
||||||
media: ThreeJsMedia
|
media: ThreeJsMedia
|
||||||
waitingChunksToDisplay = {} as { [chunkKey: string]: SectionKey[] }
|
waitingChunksToDisplay = {} as { [chunkKey: string]: SectionKey[] }
|
||||||
|
waypoints: WaypointsRenderer
|
||||||
camera: THREE.PerspectiveCamera
|
camera: THREE.PerspectiveCamera
|
||||||
renderTimeAvg = 0
|
renderTimeAvg = 0
|
||||||
sectionsOffsetsAnimations = {} as {
|
sectionsOffsetsAnimations = {} as {
|
||||||
|
|
@ -69,6 +72,11 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fountains: Fountain[] = []
|
fountains: Fountain[] = []
|
||||||
|
DEBUG_RAYCAST = false
|
||||||
|
skyboxRenderer: SkyboxRenderer
|
||||||
|
|
||||||
|
private currentPosTween?: tweenJs.Tween<THREE.Vector3>
|
||||||
|
private currentRotTween?: tweenJs.Tween<{ pitch: number, yaw: number }>
|
||||||
|
|
||||||
get tilesRendered () {
|
get tilesRendered () {
|
||||||
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).tilesCount, 0)
|
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).tilesCount, 0)
|
||||||
|
|
@ -82,11 +90,17 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
if (!initOptions.resourcesManager) throw new Error('resourcesManager is required')
|
if (!initOptions.resourcesManager) throw new Error('resourcesManager is required')
|
||||||
super(initOptions.resourcesManager, displayOptions, initOptions)
|
super(initOptions.resourcesManager, displayOptions, initOptions)
|
||||||
|
|
||||||
|
this.renderer = renderer
|
||||||
displayOptions.rendererState.renderer = WorldRendererThree.getRendererInfo(renderer) ?? '...'
|
displayOptions.rendererState.renderer = WorldRendererThree.getRendererInfo(renderer) ?? '...'
|
||||||
this.starField = new StarField(this.scene)
|
this.starField = new StarField(this)
|
||||||
|
this.cursorBlock = new CursorBlock(this)
|
||||||
this.holdingBlock = new HoldingBlock(this)
|
this.holdingBlock = new HoldingBlock(this)
|
||||||
this.holdingBlockLeft = new HoldingBlock(this, true)
|
this.holdingBlockLeft = new HoldingBlock(this, true)
|
||||||
|
|
||||||
|
// Initialize skybox renderer
|
||||||
|
this.skyboxRenderer = new SkyboxRenderer(this.scene, this.worldRendererConfig.defaultSkybox, null)
|
||||||
|
void this.skyboxRenderer.init()
|
||||||
|
|
||||||
this.addDebugOverlay()
|
this.addDebugOverlay()
|
||||||
this.resetScene()
|
this.resetScene()
|
||||||
void this.init()
|
void this.init()
|
||||||
|
|
@ -94,6 +108,8 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
this.soundSystem = new ThreeJsSound(this)
|
this.soundSystem = new ThreeJsSound(this)
|
||||||
this.cameraShake = new CameraShake(this, this.onRender)
|
this.cameraShake = new CameraShake(this, this.onRender)
|
||||||
this.media = new ThreeJsMedia(this)
|
this.media = new ThreeJsMedia(this)
|
||||||
|
this.waypoints = new WaypointsRenderer(this)
|
||||||
|
|
||||||
// this.fountain = new Fountain(this.scene, this.scene, {
|
// this.fountain = new Fountain(this.scene, this.scene, {
|
||||||
// position: new THREE.Vector3(0, 10, 0),
|
// position: new THREE.Vector3(0, 10, 0),
|
||||||
// })
|
// })
|
||||||
|
|
@ -105,7 +121,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
}
|
}
|
||||||
|
|
||||||
get cameraObject () {
|
get cameraObject () {
|
||||||
return this.cameraGroupVr || this.camera
|
return this.cameraGroupVr ?? this.cameraContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
worldSwitchActions () {
|
worldSwitchActions () {
|
||||||
|
|
@ -114,6 +130,8 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
this.protocolCustomBlocks.clear()
|
this.protocolCustomBlocks.clear()
|
||||||
// Reset section animations
|
// Reset section animations
|
||||||
this.sectionsOffsetsAnimations = {}
|
this.sectionsOffsetsAnimations = {}
|
||||||
|
// Clear waypoints
|
||||||
|
this.waypoints.clear()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,6 +152,10 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updatePlayerEntity (e: any) {
|
||||||
|
this.entities.handlePlayerEntity(e)
|
||||||
|
}
|
||||||
|
|
||||||
resetScene () {
|
resetScene () {
|
||||||
this.scene.matrixAutoUpdate = false // for perf
|
this.scene.matrixAutoUpdate = false // for perf
|
||||||
this.scene.background = new THREE.Color(this.initOptions.config.sceneBackground)
|
this.scene.background = new THREE.Color(this.initOptions.config.sceneBackground)
|
||||||
|
|
@ -144,27 +166,39 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
|
|
||||||
const size = this.renderer.getSize(new THREE.Vector2())
|
const size = this.renderer.getSize(new THREE.Vector2())
|
||||||
this.camera = new THREE.PerspectiveCamera(75, size.x / size.y, 0.1, 1000)
|
this.camera = new THREE.PerspectiveCamera(75, size.x / size.y, 0.1, 1000)
|
||||||
|
this.cameraContainer = new THREE.Object3D()
|
||||||
|
this.cameraContainer.add(this.camera)
|
||||||
|
this.scene.add(this.cameraContainer)
|
||||||
}
|
}
|
||||||
|
|
||||||
override watchReactivePlayerState () {
|
override watchReactivePlayerState () {
|
||||||
super.watchReactivePlayerState()
|
super.watchReactivePlayerState()
|
||||||
this.onReactiveValueUpdated('inWater', (value) => {
|
this.onReactivePlayerStateUpdated('inWater', (value) => {
|
||||||
this.scene.fog = value ? new THREE.Fog(0x00_00_ff, 0.1, this.displayOptions.playerState.reactive.waterBreathing ? 100 : 20) : null
|
this.skyboxRenderer.updateWaterState(value, this.playerStateReactive.waterBreathing)
|
||||||
})
|
})
|
||||||
this.onReactiveValueUpdated('ambientLight', (value) => {
|
this.onReactivePlayerStateUpdated('waterBreathing', (value) => {
|
||||||
|
this.skyboxRenderer.updateWaterState(this.playerStateReactive.inWater, value)
|
||||||
|
})
|
||||||
|
this.onReactivePlayerStateUpdated('ambientLight', (value) => {
|
||||||
if (!value) return
|
if (!value) return
|
||||||
this.ambientLight.intensity = value
|
this.ambientLight.intensity = value
|
||||||
})
|
})
|
||||||
this.onReactiveValueUpdated('directionalLight', (value) => {
|
this.onReactivePlayerStateUpdated('directionalLight', (value) => {
|
||||||
if (!value) return
|
if (!value) return
|
||||||
this.directionalLight.intensity = value
|
this.directionalLight.intensity = value
|
||||||
})
|
})
|
||||||
this.onReactiveValueUpdated('lookingAtBlock', (value) => {
|
this.onReactivePlayerStateUpdated('lookingAtBlock', (value) => {
|
||||||
this.cursorBlock.setHighlightCursorBlock(value ? new Vec3(value.x, value.y, value.z) : null, value?.shapes)
|
this.cursorBlock.setHighlightCursorBlock(value ? new Vec3(value.x, value.y, value.z) : null, value?.shapes)
|
||||||
})
|
})
|
||||||
this.onReactiveValueUpdated('diggingBlock', (value) => {
|
this.onReactivePlayerStateUpdated('diggingBlock', (value) => {
|
||||||
this.cursorBlock.updateBreakAnimation(value ? { x: value.x, y: value.y, z: value.z } : undefined, value?.stage ?? null, value?.mergedShape)
|
this.cursorBlock.updateBreakAnimation(value ? { x: value.x, y: value.y, z: value.z } : undefined, value?.stage ?? null, value?.mergedShape)
|
||||||
})
|
})
|
||||||
|
this.onReactivePlayerStateUpdated('perspective', (value) => {
|
||||||
|
// Update camera perspective when it changes
|
||||||
|
const vecPos = new Vec3(this.cameraObject.position.x, this.cameraObject.position.y, this.cameraObject.position.z)
|
||||||
|
this.updateCamera(vecPos, this.cameraShake.getBaseRotation().yaw, this.cameraShake.getBaseRotation().pitch)
|
||||||
|
// todo also update camera when block within camera was changed
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override watchReactiveConfig () {
|
override watchReactiveConfig () {
|
||||||
|
|
@ -172,6 +206,9 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
this.onReactiveConfigUpdated('showChunkBorders', (value) => {
|
this.onReactiveConfigUpdated('showChunkBorders', (value) => {
|
||||||
this.updateShowChunksBorder(value)
|
this.updateShowChunksBorder(value)
|
||||||
})
|
})
|
||||||
|
this.onReactiveConfigUpdated('defaultSkybox', (value) => {
|
||||||
|
this.skyboxRenderer.updateDefaultSkybox(value)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
changeHandSwingingState (isAnimationPlaying: boolean, isLeft = false) {
|
changeHandSwingingState (isAnimationPlaying: boolean, isLeft = false) {
|
||||||
|
|
@ -184,20 +221,18 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateAssetsData (): Promise<void> {
|
async updateAssetsData (): Promise<void> {
|
||||||
const resources = this.resourcesManager.currentResources!
|
const resources = this.resourcesManager.currentResources
|
||||||
|
|
||||||
const oldTexture = this.material.map
|
const oldTexture = this.material.map
|
||||||
const oldItemsTexture = this.itemsTexture
|
const oldItemsTexture = this.itemsTexture
|
||||||
|
|
||||||
const texture = await new THREE.TextureLoader().loadAsync(resources.blocksAtlasParser.latestImage)
|
const texture = loadThreeJsTextureFromBitmap(resources.blocksAtlasImage)
|
||||||
texture.magFilter = THREE.NearestFilter
|
texture.needsUpdate = true
|
||||||
texture.minFilter = THREE.NearestFilter
|
|
||||||
texture.flipY = false
|
texture.flipY = false
|
||||||
this.material.map = texture
|
this.material.map = texture
|
||||||
|
|
||||||
const itemsTexture = await new THREE.TextureLoader().loadAsync(resources.itemsAtlasParser.latestImage)
|
const itemsTexture = loadThreeJsTextureFromBitmap(resources.itemsAtlasImage)
|
||||||
itemsTexture.magFilter = THREE.NearestFilter
|
itemsTexture.needsUpdate = true
|
||||||
itemsTexture.minFilter = THREE.NearestFilter
|
|
||||||
itemsTexture.flipY = false
|
itemsTexture.flipY = false
|
||||||
this.itemsTexture = itemsTexture
|
this.itemsTexture = itemsTexture
|
||||||
|
|
||||||
|
|
@ -236,10 +271,23 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
} else {
|
} else {
|
||||||
this.starField.remove()
|
this.starField.remove()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.skyboxRenderer.updateTime(newTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
biomeUpdated (biome: Biome): void {
|
||||||
|
if (biome?.temperature !== undefined) {
|
||||||
|
this.skyboxRenderer.updateTemperature(biome.temperature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
biomeReset (): void {
|
||||||
|
// Reset to default temperature when biome is unknown
|
||||||
|
this.skyboxRenderer.updateTemperature(DEFAULT_TEMPERATURE)
|
||||||
}
|
}
|
||||||
|
|
||||||
getItemRenderData (item: Record<string, any>, specificProps: ItemSpecificContextProperties) {
|
getItemRenderData (item: Record<string, any>, specificProps: ItemSpecificContextProperties) {
|
||||||
return getItemUv(item, specificProps, this.resourcesManager)
|
return getItemUv(item, specificProps, this.resourcesManager, this.playerStateReactive)
|
||||||
}
|
}
|
||||||
|
|
||||||
async demoModel () {
|
async demoModel () {
|
||||||
|
|
@ -301,10 +349,11 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
section.renderOrder = 500 - chunkDistance
|
section.renderOrder = 500 - chunkDistance
|
||||||
}
|
}
|
||||||
|
|
||||||
updateViewerPosition (pos: Vec3): void {
|
override updateViewerPosition (pos: Vec3): void {
|
||||||
this.viewerPosition = pos
|
this.viewerChunkPosition = pos
|
||||||
const cameraPos = this.cameraObject.position.toArray().map(x => Math.floor(x / 16)) as [number, number, number]
|
}
|
||||||
this.cameraSectionPos = new Vec3(...cameraPos)
|
|
||||||
|
cameraSectionPositionUpdate () {
|
||||||
// eslint-disable-next-line guard-for-in
|
// eslint-disable-next-line guard-for-in
|
||||||
for (const key in this.sectionObjects) {
|
for (const key in this.sectionObjects) {
|
||||||
const value = this.sectionObjects[key]
|
const value = this.sectionObjects[key]
|
||||||
|
|
@ -408,7 +457,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
this.scene.add(object)
|
this.scene.add(object)
|
||||||
}
|
}
|
||||||
|
|
||||||
getSignTexture (position: Vec3, blockEntity, backSide = false) {
|
getSignTexture (position: Vec3, blockEntity, isHanging, backSide = false) {
|
||||||
const chunk = chunkPos(position)
|
const chunk = chunkPos(position)
|
||||||
let textures = this.chunkTextures.get(`${chunk[0]},${chunk[1]}`)
|
let textures = this.chunkTextures.get(`${chunk[0]},${chunk[1]}`)
|
||||||
if (!textures) {
|
if (!textures) {
|
||||||
|
|
@ -420,7 +469,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
if (textures[texturekey]) return textures[texturekey]
|
if (textures[texturekey]) return textures[texturekey]
|
||||||
|
|
||||||
const PrismarineChat = PrismarineChatLoader(this.version)
|
const PrismarineChat = PrismarineChatLoader(this.version)
|
||||||
const canvas = renderSign(blockEntity, PrismarineChat)
|
const canvas = renderSign(blockEntity, isHanging, PrismarineChat)
|
||||||
if (!canvas) return
|
if (!canvas) return
|
||||||
const tex = new THREE.Texture(canvas)
|
const tex = new THREE.Texture(canvas)
|
||||||
tex.magFilter = THREE.NearestFilter
|
tex.magFilter = THREE.NearestFilter
|
||||||
|
|
@ -430,13 +479,149 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
return tex
|
return tex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCameraPosition () {
|
||||||
|
const worldPos = new THREE.Vector3()
|
||||||
|
this.camera.getWorldPosition(worldPos)
|
||||||
|
return worldPos
|
||||||
|
}
|
||||||
|
|
||||||
|
getSectionCameraPosition () {
|
||||||
|
const pos = this.getCameraPosition()
|
||||||
|
return new Vec3(
|
||||||
|
Math.floor(pos.x / 16),
|
||||||
|
Math.floor(pos.y / 16),
|
||||||
|
Math.floor(pos.z / 16)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCameraSectionPos () {
|
||||||
|
const newSectionPos = this.getSectionCameraPosition()
|
||||||
|
if (!this.cameraSectionPos.equals(newSectionPos)) {
|
||||||
|
this.cameraSectionPos = newSectionPos
|
||||||
|
this.cameraSectionPositionUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number) {
|
setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number) {
|
||||||
const yOffset = this.displayOptions.playerState.getEyeHeight()
|
const yOffset = this.playerStateReactive.eyeHeight
|
||||||
|
|
||||||
this.updateCamera(pos?.offset(0, yOffset, 0) ?? null, yaw, pitch)
|
this.updateCamera(pos?.offset(0, yOffset, 0) ?? null, yaw, pitch)
|
||||||
this.media.tryIntersectMedia()
|
this.media.tryIntersectMedia()
|
||||||
|
this.updateCameraSectionPos()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getThirdPersonCamera (pos: THREE.Vector3 | null, yaw: number, pitch: number) {
|
||||||
|
pos ??= this.cameraObject.position
|
||||||
|
|
||||||
|
// Calculate camera offset based on perspective
|
||||||
|
const isBack = this.playerStateReactive.perspective === 'third_person_back'
|
||||||
|
const distance = 4 // Default third person distance
|
||||||
|
|
||||||
|
// Calculate direction vector using proper world orientation
|
||||||
|
// We need to get the camera's current look direction and use that for positioning
|
||||||
|
|
||||||
|
// Create a direction vector that represents where the camera is looking
|
||||||
|
// This matches the Three.js camera coordinate system
|
||||||
|
const direction = new THREE.Vector3(0, 0, -1) // Forward direction in camera space
|
||||||
|
|
||||||
|
// Apply the same rotation that's applied to the camera container
|
||||||
|
const pitchQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), pitch)
|
||||||
|
const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), yaw)
|
||||||
|
const finalQuat = new THREE.Quaternion().multiplyQuaternions(yawQuat, pitchQuat)
|
||||||
|
|
||||||
|
// Transform the direction vector by the camera's rotation
|
||||||
|
direction.applyQuaternion(finalQuat)
|
||||||
|
|
||||||
|
// For back view, we want the camera behind the player (opposite to view direction)
|
||||||
|
// For front view, we want the camera in front of the player (same as view direction)
|
||||||
|
if (isBack) {
|
||||||
|
direction.multiplyScalar(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create debug visualization if advanced stats are enabled
|
||||||
|
if (this.DEBUG_RAYCAST) {
|
||||||
|
this.debugRaycast(pos, direction, distance)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform raycast to avoid camera going through blocks
|
||||||
|
const raycaster = new THREE.Raycaster()
|
||||||
|
raycaster.set(pos, direction)
|
||||||
|
raycaster.far = distance // Limit raycast distance
|
||||||
|
|
||||||
|
// Filter to only nearby chunks for performance
|
||||||
|
const nearbyChunks = Object.values(this.sectionObjects)
|
||||||
|
.filter(obj => obj.name === 'chunk' && obj.visible)
|
||||||
|
.filter(obj => {
|
||||||
|
// Get the mesh child which has the actual geometry
|
||||||
|
const mesh = obj.children.find(child => child.name === 'mesh')
|
||||||
|
if (!mesh) return false
|
||||||
|
|
||||||
|
// Check distance from player position to chunk
|
||||||
|
const chunkWorldPos = new THREE.Vector3()
|
||||||
|
mesh.getWorldPosition(chunkWorldPos)
|
||||||
|
const distance = pos.distanceTo(chunkWorldPos)
|
||||||
|
return distance < 80 // Only check chunks within 80 blocks
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get all mesh children for raycasting
|
||||||
|
const meshes: THREE.Object3D[] = []
|
||||||
|
for (const chunk of nearbyChunks) {
|
||||||
|
const mesh = chunk.children.find(child => child.name === 'mesh')
|
||||||
|
if (mesh) meshes.push(mesh)
|
||||||
|
}
|
||||||
|
|
||||||
|
const intersects = raycaster.intersectObjects(meshes, false)
|
||||||
|
|
||||||
|
let finalDistance = distance
|
||||||
|
if (intersects.length > 0) {
|
||||||
|
// Use intersection distance minus a small offset to prevent clipping
|
||||||
|
finalDistance = Math.max(0.5, intersects[0].distance - 0.2)
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalPos = new Vec3(
|
||||||
|
pos.x + direction.x * finalDistance,
|
||||||
|
pos.y + direction.y * finalDistance,
|
||||||
|
pos.z + direction.z * finalDistance
|
||||||
|
)
|
||||||
|
|
||||||
|
return finalPos
|
||||||
|
}
|
||||||
|
|
||||||
|
private debugRaycastHelper?: THREE.ArrowHelper
|
||||||
|
private debugHitPoint?: THREE.Mesh
|
||||||
|
|
||||||
|
private debugRaycast (pos: THREE.Vector3, direction: THREE.Vector3, distance: number) {
|
||||||
|
// Remove existing debug objects
|
||||||
|
if (this.debugRaycastHelper) {
|
||||||
|
this.scene.remove(this.debugRaycastHelper)
|
||||||
|
this.debugRaycastHelper = undefined
|
||||||
|
}
|
||||||
|
if (this.debugHitPoint) {
|
||||||
|
this.scene.remove(this.debugHitPoint)
|
||||||
|
this.debugHitPoint = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create raycast arrow
|
||||||
|
this.debugRaycastHelper = new THREE.ArrowHelper(
|
||||||
|
direction.clone().normalize(),
|
||||||
|
pos,
|
||||||
|
distance,
|
||||||
|
0xff_00_00, // Red color
|
||||||
|
distance * 0.1,
|
||||||
|
distance * 0.05
|
||||||
|
)
|
||||||
|
this.scene.add(this.debugRaycastHelper)
|
||||||
|
|
||||||
|
// Create hit point indicator
|
||||||
|
const hitGeometry = new THREE.SphereGeometry(0.2, 8, 8)
|
||||||
|
const hitMaterial = new THREE.MeshBasicMaterial({ color: 0x00_ff_00 })
|
||||||
|
this.debugHitPoint = new THREE.Mesh(hitGeometry, hitMaterial)
|
||||||
|
this.debugHitPoint.position.copy(pos).add(direction.clone().multiplyScalar(distance))
|
||||||
|
this.scene.add(this.debugHitPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
prevFramePerspective = null as string | null
|
||||||
|
|
||||||
updateCamera (pos: Vec3 | null, yaw: number, pitch: number): void {
|
updateCamera (pos: Vec3 | null, yaw: number, pitch: number): void {
|
||||||
// if (this.freeFlyMode) {
|
// if (this.freeFlyMode) {
|
||||||
// pos = this.freeFlyState.position
|
// pos = this.freeFlyState.position
|
||||||
|
|
@ -449,10 +634,66 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
pos.y -= this.camera.position.y // Fix Y position of camera in world
|
pos.y -= this.camera.position.y // Fix Y position of camera in world
|
||||||
}
|
}
|
||||||
|
|
||||||
new tweenJs.Tween(this.cameraObject.position).to({ x: pos.x, y: pos.y, z: pos.z }, 50).start()
|
this.currentPosTween?.stop()
|
||||||
|
this.currentPosTween = new tweenJs.Tween(this.cameraObject.position).to({ x: pos.x, y: pos.y, z: pos.z }, this.playerStateUtils.isSpectatingEntity() ? 150 : 50).start()
|
||||||
// this.freeFlyState.position = pos
|
// this.freeFlyState.position = pos
|
||||||
}
|
}
|
||||||
this.cameraShake.setBaseRotation(pitch, yaw)
|
|
||||||
|
if (this.playerStateUtils.isSpectatingEntity()) {
|
||||||
|
const rotation = this.cameraShake.getBaseRotation()
|
||||||
|
// wrap in the correct direction
|
||||||
|
let yawOffset = 0
|
||||||
|
const halfPi = Math.PI / 2
|
||||||
|
if (rotation.yaw < halfPi && yaw > Math.PI + halfPi) {
|
||||||
|
yawOffset = -Math.PI * 2
|
||||||
|
} else if (yaw < halfPi && rotation.yaw > Math.PI + halfPi) {
|
||||||
|
yawOffset = Math.PI * 2
|
||||||
|
}
|
||||||
|
this.currentRotTween?.stop()
|
||||||
|
this.currentRotTween = new tweenJs.Tween(rotation).to({ pitch, yaw: yaw + yawOffset }, 100)
|
||||||
|
.onUpdate(params => this.cameraShake.setBaseRotation(params.pitch, params.yaw - yawOffset)).start()
|
||||||
|
} else {
|
||||||
|
this.currentRotTween?.stop()
|
||||||
|
this.cameraShake.setBaseRotation(pitch, yaw)
|
||||||
|
|
||||||
|
const { perspective } = this.playerStateReactive
|
||||||
|
if (perspective === 'third_person_back' || perspective === 'third_person_front') {
|
||||||
|
// Use getThirdPersonCamera for proper raycasting with max distance of 4
|
||||||
|
const currentCameraPos = this.cameraObject.position
|
||||||
|
const thirdPersonPos = this.getThirdPersonCamera(
|
||||||
|
new THREE.Vector3(currentCameraPos.x, currentCameraPos.y, currentCameraPos.z),
|
||||||
|
yaw,
|
||||||
|
pitch
|
||||||
|
)
|
||||||
|
|
||||||
|
const distance = currentCameraPos.distanceTo(new THREE.Vector3(thirdPersonPos.x, thirdPersonPos.y, thirdPersonPos.z))
|
||||||
|
// Apply Z offset based on perspective and calculated distance
|
||||||
|
const zOffset = perspective === 'third_person_back' ? distance : -distance
|
||||||
|
this.camera.position.set(0, 0, zOffset)
|
||||||
|
|
||||||
|
if (perspective === 'third_person_front') {
|
||||||
|
// Flip camera view 180 degrees around Y axis for front view
|
||||||
|
this.camera.rotation.set(0, Math.PI, 0)
|
||||||
|
} else {
|
||||||
|
this.camera.rotation.set(0, 0, 0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.camera.position.set(0, 0, 0)
|
||||||
|
this.camera.rotation.set(0, 0, 0)
|
||||||
|
|
||||||
|
// remove any debug raycasting
|
||||||
|
if (this.debugRaycastHelper) {
|
||||||
|
this.scene.remove(this.debugRaycastHelper)
|
||||||
|
this.debugRaycastHelper = undefined
|
||||||
|
}
|
||||||
|
if (this.debugHitPoint) {
|
||||||
|
this.scene.remove(this.debugHitPoint)
|
||||||
|
this.debugHitPoint = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateCameraSectionPos()
|
||||||
}
|
}
|
||||||
|
|
||||||
debugChunksVisibilityOverride () {
|
debugChunksVisibilityOverride () {
|
||||||
|
|
@ -493,9 +734,14 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
this.cursorBlock.render()
|
this.cursorBlock.render()
|
||||||
this.updateSectionOffsets()
|
this.updateSectionOffsets()
|
||||||
|
|
||||||
|
// Update skybox position to follow camera
|
||||||
|
const cameraPos = this.getCameraPosition()
|
||||||
|
this.skyboxRenderer.update(cameraPos, this.viewDistance)
|
||||||
|
|
||||||
const sizeOrFovChanged = sizeChanged || this.displayOptions.inWorldRenderingConfig.fov !== this.camera.fov
|
const sizeOrFovChanged = sizeChanged || this.displayOptions.inWorldRenderingConfig.fov !== this.camera.fov
|
||||||
if (sizeOrFovChanged) {
|
if (sizeOrFovChanged) {
|
||||||
this.camera.aspect = window.innerWidth / window.innerHeight
|
const size = this.renderer.getSize(new THREE.Vector2())
|
||||||
|
this.camera.aspect = size.width / size.height
|
||||||
this.camera.fov = this.displayOptions.inWorldRenderingConfig.fov
|
this.camera.fov = this.displayOptions.inWorldRenderingConfig.fov
|
||||||
this.camera.updateProjectionMatrix()
|
this.camera.updateProjectionMatrix()
|
||||||
}
|
}
|
||||||
|
|
@ -508,7 +754,13 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
const cam = this.cameraGroupVr instanceof THREE.Group ? this.cameraGroupVr.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera
|
const cam = this.cameraGroupVr instanceof THREE.Group ? this.cameraGroupVr.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera
|
||||||
this.renderer.render(this.scene, cam)
|
this.renderer.render(this.scene, cam)
|
||||||
|
|
||||||
if (this.displayOptions.inWorldRenderingConfig.showHand && !this.playerState.shouldHideHand /* && !this.freeFlyMode */ && !this.renderer.xr.isPresenting) {
|
if (
|
||||||
|
this.displayOptions.inWorldRenderingConfig.showHand &&
|
||||||
|
this.playerStateReactive.gameMode !== 'spectator' &&
|
||||||
|
this.playerStateReactive.perspective === 'first_person' &&
|
||||||
|
// !this.freeFlyMode &&
|
||||||
|
!this.renderer.xr.isPresenting
|
||||||
|
) {
|
||||||
this.holdingBlock.render(this.camera, this.renderer, this.ambientLight, this.directionalLight)
|
this.holdingBlock.render(this.camera, this.renderer, this.ambientLight, this.directionalLight)
|
||||||
this.holdingBlockLeft.render(this.camera, this.renderer, this.ambientLight, this.directionalLight)
|
this.holdingBlockLeft.render(this.camera, this.renderer, this.ambientLight, this.directionalLight)
|
||||||
}
|
}
|
||||||
|
|
@ -521,6 +773,8 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
fountain.render()
|
fountain.render()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.waypoints.render()
|
||||||
|
|
||||||
for (const onRender of this.onRender) {
|
for (const onRender of this.onRender) {
|
||||||
onRender()
|
onRender()
|
||||||
}
|
}
|
||||||
|
|
@ -533,12 +787,22 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderHead (position: Vec3, rotation: number, isWall: boolean, blockEntity) {
|
renderHead (position: Vec3, rotation: number, isWall: boolean, blockEntity) {
|
||||||
const textures = blockEntity.SkullOwner?.Properties?.textures[0]
|
let textureData: string
|
||||||
if (!textures) return
|
if (blockEntity.SkullOwner) {
|
||||||
|
textureData = blockEntity.SkullOwner.Properties?.textures?.[0]?.Value
|
||||||
|
} else {
|
||||||
|
textureData = blockEntity.profile?.properties?.find(p => p.name === 'textures')?.value
|
||||||
|
}
|
||||||
|
if (!textureData) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const textureData = JSON.parse(Buffer.from(textures.Value, 'base64').toString())
|
const decodedData = JSON.parse(Buffer.from(textureData, 'base64').toString())
|
||||||
const skinUrl = textureData.textures?.SKIN?.url
|
let skinUrl = decodedData.textures?.SKIN?.url
|
||||||
|
const { skinTexturesProxy } = this.worldRendererConfig
|
||||||
|
if (skinTexturesProxy) {
|
||||||
|
skinUrl = skinUrl?.replace('http://textures.minecraft.net/', skinTexturesProxy)
|
||||||
|
.replace('https://textures.minecraft.net/', skinTexturesProxy)
|
||||||
|
}
|
||||||
|
|
||||||
const mesh = getMesh(this, skinUrl, armorModel.head)
|
const mesh = getMesh(this, skinUrl, armorModel.head)
|
||||||
const group = new THREE.Group()
|
const group = new THREE.Group()
|
||||||
|
|
@ -562,7 +826,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSign (position: Vec3, rotation: number, isWall: boolean, isHanging: boolean, blockEntity) {
|
renderSign (position: Vec3, rotation: number, isWall: boolean, isHanging: boolean, blockEntity) {
|
||||||
const tex = this.getSignTexture(position, blockEntity)
|
const tex = this.getSignTexture(position, blockEntity, isHanging)
|
||||||
|
|
||||||
if (!tex) return
|
if (!tex) return
|
||||||
|
|
||||||
|
|
@ -633,6 +897,16 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
for (const mesh of Object.values(this.sectionObjects)) {
|
for (const mesh of Object.values(this.sectionObjects)) {
|
||||||
this.scene.remove(mesh)
|
this.scene.remove(mesh)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up debug objects
|
||||||
|
if (this.debugRaycastHelper) {
|
||||||
|
this.scene.remove(this.debugRaycastHelper)
|
||||||
|
this.debugRaycastHelper = undefined
|
||||||
|
}
|
||||||
|
if (this.debugHitPoint) {
|
||||||
|
this.scene.remove(this.debugHitPoint)
|
||||||
|
this.debugHitPoint = undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getLoadedChunksRelative (pos: Vec3, includeY = false) {
|
getLoadedChunksRelative (pos: Vec3, includeY = false) {
|
||||||
|
|
@ -708,6 +982,19 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
|
|
||||||
destroy (): void {
|
destroy (): void {
|
||||||
super.destroy()
|
super.destroy()
|
||||||
|
this.skyboxRenderer.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldObjectVisible (object: THREE.Object3D) {
|
||||||
|
// Get chunk coordinates
|
||||||
|
const chunkX = Math.floor(object.position.x / 16) * 16
|
||||||
|
const chunkZ = Math.floor(object.position.z / 16) * 16
|
||||||
|
const sectionY = Math.floor(object.position.y / 16) * 16
|
||||||
|
|
||||||
|
const chunkKey = `${chunkX},${chunkZ}`
|
||||||
|
const sectionKey = `${chunkX},${sectionY},${chunkZ}`
|
||||||
|
|
||||||
|
return !!this.finishedChunks[chunkKey] || !!this.sectionObjects[sectionKey]
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSectionOffsets () {
|
updateSectionOffsets () {
|
||||||
|
|
@ -756,6 +1043,10 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reloadWorld () {
|
||||||
|
this.entities.reloadEntities()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class StarField {
|
class StarField {
|
||||||
|
|
@ -772,7 +1063,16 @@ class StarField {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor (private readonly scene: THREE.Scene) {
|
constructor (
|
||||||
|
private readonly worldRenderer: WorldRendererThree
|
||||||
|
) {
|
||||||
|
const clock = new THREE.Clock()
|
||||||
|
const speed = 0.2
|
||||||
|
this.worldRenderer.onRender.push(() => {
|
||||||
|
if (!this.points) return
|
||||||
|
this.points.position.copy(this.worldRenderer.getCameraPosition());
|
||||||
|
(this.points.material as StarfieldMaterial).uniforms.time.value = clock.getElapsedTime() * speed
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
addToScene () {
|
addToScene () {
|
||||||
|
|
@ -783,7 +1083,6 @@ class StarField {
|
||||||
const count = 7000
|
const count = 7000
|
||||||
const factor = 7
|
const factor = 7
|
||||||
const saturation = 10
|
const saturation = 10
|
||||||
const speed = 0.2
|
|
||||||
|
|
||||||
const geometry = new THREE.BufferGeometry()
|
const geometry = new THREE.BufferGeometry()
|
||||||
|
|
||||||
|
|
@ -814,13 +1113,8 @@ class StarField {
|
||||||
|
|
||||||
// Create points and add them to the scene
|
// Create points and add them to the scene
|
||||||
this.points = new THREE.Points(geometry, material)
|
this.points = new THREE.Points(geometry, material)
|
||||||
this.scene.add(this.points)
|
this.worldRenderer.scene.add(this.points)
|
||||||
|
|
||||||
const clock = new THREE.Clock()
|
|
||||||
this.points.onBeforeRender = (renderer, scene, camera) => {
|
|
||||||
this.points?.position.copy?.(camera.position)
|
|
||||||
material.uniforms.time.value = clock.getElapsedTime() * speed
|
|
||||||
}
|
|
||||||
this.points.renderOrder = -1
|
this.points.renderOrder = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -828,7 +1122,7 @@ class StarField {
|
||||||
if (this.points) {
|
if (this.points) {
|
||||||
this.points.geometry.dispose();
|
this.points.geometry.dispose();
|
||||||
(this.points.material as THREE.Material).dispose()
|
(this.points.material as THREE.Material).dispose()
|
||||||
this.scene.remove(this.points)
|
this.worldRenderer.scene.remove(this.points)
|
||||||
|
|
||||||
this.points = undefined
|
this.points = undefined
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
/// <reference types="./src/env" />
|
||||||
import { defineConfig, mergeRsbuildConfig, RsbuildPluginAPI } from '@rsbuild/core'
|
import { defineConfig, mergeRsbuildConfig, RsbuildPluginAPI } from '@rsbuild/core'
|
||||||
import { pluginReact } from '@rsbuild/plugin-react'
|
import { pluginReact } from '@rsbuild/plugin-react'
|
||||||
import { pluginTypedCSSModules } from '@rsbuild/plugin-typed-css-modules'
|
import { pluginTypedCSSModules } from '@rsbuild/plugin-typed-css-modules'
|
||||||
|
|
@ -14,6 +15,7 @@ import { appAndRendererSharedConfig } from './renderer/rsbuildSharedConfig'
|
||||||
import { genLargeDataAliases } from './scripts/genLargeDataAliases'
|
import { genLargeDataAliases } from './scripts/genLargeDataAliases'
|
||||||
import sharp from 'sharp'
|
import sharp from 'sharp'
|
||||||
import supportedVersions from './src/supportedVersions.mjs'
|
import supportedVersions from './src/supportedVersions.mjs'
|
||||||
|
import { startWsServer } from './scripts/wsServer'
|
||||||
|
|
||||||
const SINGLE_FILE_BUILD = process.env.SINGLE_FILE_BUILD === 'true'
|
const SINGLE_FILE_BUILD = process.env.SINGLE_FILE_BUILD === 'true'
|
||||||
|
|
||||||
|
|
@ -48,7 +50,7 @@ if (fs.existsSync('./assets/release.json')) {
|
||||||
|
|
||||||
const configJson = JSON.parse(fs.readFileSync('./config.json', 'utf8'))
|
const configJson = JSON.parse(fs.readFileSync('./config.json', 'utf8'))
|
||||||
try {
|
try {
|
||||||
Object.assign(configJson, JSON.parse(fs.readFileSync('./config.local.json', 'utf8')))
|
Object.assign(configJson, JSON.parse(fs.readFileSync(process.env.LOCAL_CONFIG_FILE || './config.local.json', 'utf8')))
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
if (dev) {
|
if (dev) {
|
||||||
configJson.defaultProxy = ':8080'
|
configJson.defaultProxy = ':8080'
|
||||||
|
|
@ -58,6 +60,8 @@ const configSource = (SINGLE_FILE_BUILD ? 'BUNDLED' : (process.env.CONFIG_JSON_S
|
||||||
|
|
||||||
const faviconPath = 'favicon.png'
|
const faviconPath = 'favicon.png'
|
||||||
|
|
||||||
|
const enableMetrics = process.env.ENABLE_METRICS === 'true'
|
||||||
|
|
||||||
// base options are in ./renderer/rsbuildSharedConfig.ts
|
// base options are in ./renderer/rsbuildSharedConfig.ts
|
||||||
const appConfig = defineConfig({
|
const appConfig = defineConfig({
|
||||||
html: {
|
html: {
|
||||||
|
|
@ -111,6 +115,22 @@ const appConfig = defineConfig({
|
||||||
js: 'source-map',
|
js: 'source-map',
|
||||||
css: true,
|
css: true,
|
||||||
},
|
},
|
||||||
|
minify: {
|
||||||
|
// js: false,
|
||||||
|
jsOptions: {
|
||||||
|
minimizerOptions: {
|
||||||
|
mangle: {
|
||||||
|
safari10: true,
|
||||||
|
keep_classnames: true,
|
||||||
|
keep_fnames: true,
|
||||||
|
keep_private_props: true,
|
||||||
|
},
|
||||||
|
compress: {
|
||||||
|
unused: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
distPath: SINGLE_FILE_BUILD ? {
|
distPath: SINGLE_FILE_BUILD ? {
|
||||||
html: './single',
|
html: './single',
|
||||||
} : undefined,
|
} : undefined,
|
||||||
|
|
@ -119,6 +139,13 @@ const appConfig = defineConfig({
|
||||||
// 50kb limit for data uri
|
// 50kb limit for data uri
|
||||||
dataUriLimit: SINGLE_FILE_BUILD ? 1 * 1024 * 1024 * 1024 : 50 * 1024
|
dataUriLimit: SINGLE_FILE_BUILD ? 1 * 1024 * 1024 * 1024 : 50 * 1024
|
||||||
},
|
},
|
||||||
|
performance: {
|
||||||
|
// prefetch: {
|
||||||
|
// include(filename) {
|
||||||
|
// return filename.includes('mc-data') || filename.includes('mc-assets')
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
},
|
||||||
source: {
|
source: {
|
||||||
entry: {
|
entry: {
|
||||||
index: './src/index.ts',
|
index: './src/index.ts',
|
||||||
|
|
@ -134,12 +161,15 @@ const appConfig = defineConfig({
|
||||||
'process.platform': '"browser"',
|
'process.platform': '"browser"',
|
||||||
'process.env.GITHUB_URL':
|
'process.env.GITHUB_URL':
|
||||||
JSON.stringify(`https://github.com/${process.env.GITHUB_REPOSITORY || `${process.env.VERCEL_GIT_REPO_OWNER}/${process.env.VERCEL_GIT_REPO_SLUG}` || githubRepositoryFallback}`),
|
JSON.stringify(`https://github.com/${process.env.GITHUB_REPOSITORY || `${process.env.VERCEL_GIT_REPO_OWNER}/${process.env.VERCEL_GIT_REPO_SLUG}` || githubRepositoryFallback}`),
|
||||||
'process.env.DEPS_VERSIONS': JSON.stringify({}),
|
'process.env.ALWAYS_MINIMAL_SERVER_UI': JSON.stringify(process.env.ALWAYS_MINIMAL_SERVER_UI),
|
||||||
'process.env.RELEASE_TAG': JSON.stringify(releaseTag),
|
'process.env.RELEASE_TAG': JSON.stringify(releaseTag),
|
||||||
'process.env.RELEASE_LINK': JSON.stringify(releaseLink),
|
'process.env.RELEASE_LINK': JSON.stringify(releaseLink),
|
||||||
'process.env.RELEASE_CHANGELOG': JSON.stringify(releaseChangelog),
|
'process.env.RELEASE_CHANGELOG': JSON.stringify(releaseChangelog),
|
||||||
'process.env.DISABLE_SERVICE_WORKER': JSON.stringify(disableServiceWorker),
|
'process.env.DISABLE_SERVICE_WORKER': JSON.stringify(disableServiceWorker),
|
||||||
'process.env.INLINED_APP_CONFIG': JSON.stringify(configSource === 'BUNDLED' ? configJson : null),
|
'process.env.INLINED_APP_CONFIG': JSON.stringify(configSource === 'BUNDLED' ? configJson : null),
|
||||||
|
'process.env.ENABLE_COOKIE_STORAGE': JSON.stringify(process.env.ENABLE_COOKIE_STORAGE || true),
|
||||||
|
'process.env.COOKIE_STORAGE_PREFIX': JSON.stringify(process.env.COOKIE_STORAGE_PREFIX || ''),
|
||||||
|
'process.env.WS_PORT': JSON.stringify(enableMetrics ? 8081 : false),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
|
|
@ -167,20 +197,21 @@ const appConfig = defineConfig({
|
||||||
childProcess.execSync('tsx ./scripts/optimizeBlockCollisions.ts', { stdio: 'inherit' })
|
childProcess.execSync('tsx ./scripts/optimizeBlockCollisions.ts', { stdio: 'inherit' })
|
||||||
}
|
}
|
||||||
// childProcess.execSync(['tsx', './scripts/genLargeDataAliases.ts', ...(SINGLE_FILE_BUILD ? ['--compressed'] : [])].join(' '), { stdio: 'inherit' })
|
// childProcess.execSync(['tsx', './scripts/genLargeDataAliases.ts', ...(SINGLE_FILE_BUILD ? ['--compressed'] : [])].join(' '), { stdio: 'inherit' })
|
||||||
genLargeDataAliases(SINGLE_FILE_BUILD)
|
genLargeDataAliases(SINGLE_FILE_BUILD || process.env.ALWAYS_COMPRESS_LARGE_DATA === 'true')
|
||||||
fsExtra.copySync('./node_modules/mc-assets/dist/other-textures/latest/entity', './dist/textures/entity')
|
fsExtra.copySync('./node_modules/mc-assets/dist/other-textures/latest/entity', './dist/textures/entity')
|
||||||
fsExtra.copySync('./assets/background', './dist/background')
|
fsExtra.copySync('./assets/background', './dist/background')
|
||||||
fs.copyFileSync('./assets/favicon.png', './dist/favicon.png')
|
fs.copyFileSync('./assets/favicon.png', './dist/favicon.png')
|
||||||
fs.copyFileSync('./assets/playground.html', './dist/playground.html')
|
fs.copyFileSync('./assets/playground.html', './dist/playground.html')
|
||||||
fs.copyFileSync('./assets/manifest.json', './dist/manifest.json')
|
fs.copyFileSync('./assets/manifest.json', './dist/manifest.json')
|
||||||
fs.copyFileSync('./assets/config.html', './dist/config.html')
|
fs.copyFileSync('./assets/config.html', './dist/config.html')
|
||||||
|
fs.copyFileSync('./assets/debug-inputs.html', './dist/debug-inputs.html')
|
||||||
fs.copyFileSync('./assets/loading-bg.jpg', './dist/loading-bg.jpg')
|
fs.copyFileSync('./assets/loading-bg.jpg', './dist/loading-bg.jpg')
|
||||||
if (fs.existsSync('./assets/release.json')) {
|
if (fs.existsSync('./assets/release.json')) {
|
||||||
fs.copyFileSync('./assets/release.json', './dist/release.json')
|
fs.copyFileSync('./assets/release.json', './dist/release.json')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (configSource === 'REMOTE') {
|
if (configSource === 'REMOTE') {
|
||||||
fs.writeFileSync('./dist/config.json', JSON.stringify(configJson), 'utf8')
|
fs.writeFileSync('./dist/config.json', JSON.stringify(configJson, undefined, 2), 'utf8')
|
||||||
}
|
}
|
||||||
if (fs.existsSync('./generated/sounds.js')) {
|
if (fs.existsSync('./generated/sounds.js')) {
|
||||||
fs.copyFileSync('./generated/sounds.js', './dist/sounds.js')
|
fs.copyFileSync('./generated/sounds.js', './dist/sounds.js')
|
||||||
|
|
@ -196,6 +227,12 @@ const appConfig = defineConfig({
|
||||||
await execAsync('pnpm run build-mesher')
|
await execAsync('pnpm run build-mesher')
|
||||||
}
|
}
|
||||||
fs.writeFileSync('./dist/version.txt', buildingVersion, 'utf-8')
|
fs.writeFileSync('./dist/version.txt', buildingVersion, 'utf-8')
|
||||||
|
|
||||||
|
// Start WebSocket server in development
|
||||||
|
if (dev && enableMetrics) {
|
||||||
|
await startWsServer(8081, false)
|
||||||
|
}
|
||||||
|
|
||||||
console.timeEnd('total-prep')
|
console.timeEnd('total-prep')
|
||||||
}
|
}
|
||||||
if (!dev) {
|
if (!dev) {
|
||||||
|
|
@ -203,6 +240,10 @@ const appConfig = defineConfig({
|
||||||
prep()
|
prep()
|
||||||
})
|
})
|
||||||
build.onAfterBuild(async () => {
|
build.onAfterBuild(async () => {
|
||||||
|
if (fs.readdirSync('./assets/customTextures').length > 0) {
|
||||||
|
childProcess.execSync('tsx ./scripts/patchAssets.ts', { stdio: 'inherit' })
|
||||||
|
}
|
||||||
|
|
||||||
if (SINGLE_FILE_BUILD) {
|
if (SINGLE_FILE_BUILD) {
|
||||||
// check that only index.html is in the dist/single folder
|
// check that only index.html is in the dist/single folder
|
||||||
const singleBuildFiles = fs.readdirSync('./dist/single')
|
const singleBuildFiles = fs.readdirSync('./dist/single')
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,8 @@ export const genLargeDataAliases = async (isCompressed: boolean) => {
|
||||||
|
|
||||||
let str = `${decoderCode}\nexport const importLargeData = async (mod: ${Object.keys(modules).map(x => `'${x}'`).join(' | ')}) => {\n`
|
let str = `${decoderCode}\nexport const importLargeData = async (mod: ${Object.keys(modules).map(x => `'${x}'`).join(' | ')}) => {\n`
|
||||||
for (const [module, { compressed, raw }] of Object.entries(modules)) {
|
for (const [module, { compressed, raw }] of Object.entries(modules)) {
|
||||||
let importCode = `(await import('${isCompressed ? compressed : raw}')).default`;
|
const chunkName = module === 'mcData' ? 'mc-data' : 'mc-assets';
|
||||||
|
let importCode = `(await import(/* webpackChunkName: "${chunkName}" */ '${isCompressed ? compressed : raw}')).default`;
|
||||||
if (isCompressed) {
|
if (isCompressed) {
|
||||||
importCode = `JSON.parse(decompressFromBase64(${importCode}))`
|
importCode = `JSON.parse(decompressFromBase64(${importCode}))`
|
||||||
}
|
}
|
||||||
|
|
@ -30,6 +31,8 @@ export const genLargeDataAliases = async (isCompressed: boolean) => {
|
||||||
const decoderCode = /* ts */ `
|
const decoderCode = /* ts */ `
|
||||||
import pako from 'pako';
|
import pako from 'pako';
|
||||||
|
|
||||||
|
globalThis.pako = { inflate: pako.inflate.bind(pako) }
|
||||||
|
|
||||||
function decompressFromBase64(input) {
|
function decompressFromBase64(input) {
|
||||||
console.time('decompressFromBase64')
|
console.time('decompressFromBase64')
|
||||||
// Decode the Base64 string
|
// Decode the Base64 string
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ import { dirname } from 'node:path'
|
||||||
import supportedVersions from '../src/supportedVersions.mjs'
|
import supportedVersions from '../src/supportedVersions.mjs'
|
||||||
import { gzipSizeFromFileSync } from 'gzip-size'
|
import { gzipSizeFromFileSync } from 'gzip-size'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import {default as _JsonOptimizer} from '../src/optimizeJson'
|
import { default as _JsonOptimizer } from '../src/optimizeJson'
|
||||||
import { gzipSync } from 'zlib';
|
import { gzipSync } from 'zlib'
|
||||||
import MinecraftData from 'minecraft-data'
|
import MinecraftData from 'minecraft-data'
|
||||||
import MCProtocol from 'minecraft-protocol'
|
import MCProtocol from 'minecraft-protocol'
|
||||||
|
|
||||||
|
|
@ -21,12 +21,12 @@ const require = Module.createRequire(import.meta.url)
|
||||||
|
|
||||||
const dataPaths = require('minecraft-data/minecraft-data/data/dataPaths.json')
|
const dataPaths = require('minecraft-data/minecraft-data/data/dataPaths.json')
|
||||||
|
|
||||||
function toMajor (version) {
|
function toMajor(version) {
|
||||||
const [a, b] = (version + '').split('.')
|
const [a, b] = (version + '').split('.')
|
||||||
return `${a}.${b}`
|
return `${a}.${b}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const versions = {}
|
let versions = {}
|
||||||
const dataTypes = new Set()
|
const dataTypes = new Set()
|
||||||
|
|
||||||
for (const [version, dataSet] of Object.entries(dataPaths.pc)) {
|
for (const [version, dataSet] of Object.entries(dataPaths.pc)) {
|
||||||
|
|
@ -42,6 +42,31 @@ const versionToNumber = (ver) => {
|
||||||
return +`${x.padStart(2, '0')}${y.padStart(2, '0')}${z.padStart(2, '0')}`
|
return +`${x.padStart(2, '0')}${y.padStart(2, '0')}${z.padStart(2, '0')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Version clipping support
|
||||||
|
const minVersion = process.env.MIN_MC_VERSION
|
||||||
|
const maxVersion = process.env.MAX_MC_VERSION
|
||||||
|
|
||||||
|
// Filter versions based on MIN_VERSION and MAX_VERSION if provided
|
||||||
|
if (minVersion || maxVersion) {
|
||||||
|
const filteredVersions = {}
|
||||||
|
const minVersionNum = minVersion ? versionToNumber(minVersion) : 0
|
||||||
|
const maxVersionNum = maxVersion ? versionToNumber(maxVersion) : Infinity
|
||||||
|
|
||||||
|
for (const [version, dataSet] of Object.entries(versions)) {
|
||||||
|
const versionNum = versionToNumber(version)
|
||||||
|
if (versionNum >= minVersionNum && versionNum <= maxVersionNum) {
|
||||||
|
filteredVersions[version] = dataSet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
versions = filteredVersions
|
||||||
|
|
||||||
|
console.log(`Version clipping applied: ${minVersion || 'none'} to ${maxVersion || 'none'}`)
|
||||||
|
console.log(`Processing ${Object.keys(versions).length} versions:`, Object.keys(versions).sort((a, b) => versionToNumber(a) - versionToNumber(b)))
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Bundling version range:', Object.keys(versions)[0], 'to', Object.keys(versions).at(-1))
|
||||||
|
|
||||||
// if not included here (even as {}) will not be bundled & accessible!
|
// if not included here (even as {}) will not be bundled & accessible!
|
||||||
// const compressedOutput = !!process.env.SINGLE_FILE_BUILD
|
// const compressedOutput = !!process.env.SINGLE_FILE_BUILD
|
||||||
const compressedOutput = true
|
const compressedOutput = true
|
||||||
|
|
@ -57,22 +82,27 @@ const dataTypeBundling2 = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const dataTypeBundling = {
|
const dataTypeBundling = {
|
||||||
language: {
|
language: process.env.SKIP_MC_DATA_LANGUAGE === 'true' ? {
|
||||||
|
raw: {}
|
||||||
|
} : {
|
||||||
ignoreRemoved: true,
|
ignoreRemoved: true,
|
||||||
ignoreChanges: true
|
ignoreChanges: true
|
||||||
},
|
},
|
||||||
blocks: {
|
blocks: {
|
||||||
arrKey: 'name',
|
arrKey: 'name',
|
||||||
processData (current, prev) {
|
processData(current, prev, _, version) {
|
||||||
for (const block of current) {
|
for (const block of current) {
|
||||||
|
const prevBlock = prev?.find(x => x.name === block.name)
|
||||||
if (block.transparent) {
|
if (block.transparent) {
|
||||||
const forceOpaque = block.name.includes('shulker_box') || block.name.match(/^double_.+_slab\d?$/) || ['melon_block', 'lit_pumpkin', 'lit_redstone_ore', 'lit_furnace'].includes(block.name)
|
const forceOpaque = block.name.includes('shulker_box') || block.name.match(/^double_.+_slab\d?$/) || ['melon_block', 'lit_pumpkin', 'lit_redstone_ore', 'lit_furnace'].includes(block.name)
|
||||||
|
|
||||||
const prevBlock = prev?.find(x => x.name === block.name);
|
|
||||||
if (forceOpaque || (prevBlock && !prevBlock.transparent)) {
|
if (forceOpaque || (prevBlock && !prevBlock.transparent)) {
|
||||||
block.transparent = false
|
block.transparent = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (block.hardness === 0 && prevBlock && prevBlock.hardness > 0) {
|
||||||
|
block.hardness = prevBlock.hardness
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// ignoreRemoved: true,
|
// ignoreRemoved: true,
|
||||||
|
|
@ -136,7 +166,9 @@ const dataTypeBundling = {
|
||||||
blockLoot: {
|
blockLoot: {
|
||||||
arrKey: 'block'
|
arrKey: 'block'
|
||||||
},
|
},
|
||||||
recipes: {
|
recipes: process.env.SKIP_MC_DATA_RECIPES === 'true' ? {
|
||||||
|
raw: {}
|
||||||
|
} : {
|
||||||
raw: true
|
raw: true
|
||||||
// processData: processRecipes
|
// processData: processRecipes
|
||||||
},
|
},
|
||||||
|
|
@ -150,7 +182,7 @@ const dataTypeBundling = {
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
function processRecipes (current, prev, getData, version) {
|
function processRecipes(current, prev, getData, version) {
|
||||||
// can require the same multiple times per different versions
|
// can require the same multiple times per different versions
|
||||||
if (current._proccessed) return
|
if (current._proccessed) return
|
||||||
const items = getData('items')
|
const items = getData('items')
|
||||||
|
|
@ -242,30 +274,39 @@ for (const [i, [version, dataSet]] of versionsArr.reverse().entries()) {
|
||||||
for (const [dataType, dataPath] of Object.entries(dataSet)) {
|
for (const [dataType, dataPath] of Object.entries(dataSet)) {
|
||||||
const config = dataTypeBundling[dataType]
|
const config = dataTypeBundling[dataType]
|
||||||
if (!config) continue
|
if (!config) continue
|
||||||
if (dataType === 'blockCollisionShapes' && versionToNumber(version) >= versionToNumber('1.13')) {
|
const ignoreCollisionShapes = dataType === 'blockCollisionShapes' && versionToNumber(version) >= versionToNumber('1.13')
|
||||||
// contents += ` get ${dataType} () { return window.globalGetCollisionShapes?.("${version}") },\n`
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
let injectCode = ''
|
let injectCode = ''
|
||||||
const getData = (type) => {
|
const getRealData = (type) => {
|
||||||
const loc = `minecraft-data/data/${dataSet[type]}/`
|
const loc = `minecraft-data/data/${dataSet[type]}/`
|
||||||
const dataPathAbsolute = require.resolve(`minecraft-data/${loc}${type}`)
|
const dataPathAbsolute = require.resolve(`minecraft-data/${loc}${type}`)
|
||||||
// const data = fs.readFileSync(dataPathAbsolute, 'utf8')
|
// const data = fs.readFileSync(dataPathAbsolute, 'utf8')
|
||||||
const dataRaw = require(dataPathAbsolute)
|
const dataRaw = require(dataPathAbsolute)
|
||||||
return dataRaw
|
return dataRaw
|
||||||
}
|
}
|
||||||
const dataRaw = getData(dataType)
|
const dataRaw = getRealData(dataType)
|
||||||
let rawData = dataRaw
|
let rawData = dataRaw
|
||||||
if (config.raw) {
|
if (config.raw) {
|
||||||
rawDataVersions[dataType] ??= {}
|
rawDataVersions[dataType] ??= {}
|
||||||
rawDataVersions[dataType][version] = rawData
|
rawDataVersions[dataType][version] = rawData
|
||||||
rawData = dataRaw
|
if (config.raw === true) {
|
||||||
|
rawData = dataRaw
|
||||||
|
} else {
|
||||||
|
rawData = config.raw
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ignoreCollisionShapes && dataType === 'blockCollisionShapes') {
|
||||||
|
rawData = {
|
||||||
|
blocks: {},
|
||||||
|
shapes: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!diffSources[dataType]) {
|
if (!diffSources[dataType]) {
|
||||||
diffSources[dataType] = new JsonOptimizer(config.arrKey, config.ignoreChanges, config.ignoreRemoved)
|
diffSources[dataType] = new JsonOptimizer(config.arrKey, config.ignoreChanges, config.ignoreRemoved)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
config.processData?.(dataRaw, previousData[dataType], getData, version)
|
config.processData?.(dataRaw, previousData[dataType], getRealData, version)
|
||||||
diffSources[dataType].recordDiff(version, dataRaw)
|
diffSources[dataType].recordDiff(version, dataRaw)
|
||||||
injectCode = `restoreDiff(sources, ${JSON.stringify(dataType)}, ${JSON.stringify(version)})`
|
injectCode = `restoreDiff(sources, ${JSON.stringify(dataType)}, ${JSON.stringify(version)})`
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -297,16 +338,16 @@ console.log('total size (mb)', totalSize / 1024 / 1024)
|
||||||
console.log(
|
console.log(
|
||||||
'size per data type (mb, %)',
|
'size per data type (mb, %)',
|
||||||
Object.fromEntries(Object.entries(sizePerDataType).map(([dataType, size]) => {
|
Object.fromEntries(Object.entries(sizePerDataType).map(([dataType, size]) => {
|
||||||
return [dataType, [size / 1024 / 1024, Math.round(size / totalSize * 100)]];
|
return [dataType, [size / 1024 / 1024, Math.round(size / totalSize * 100)]]
|
||||||
}).sort((a, b) => {
|
}).sort((a, b) => {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
return b[1][1] - a[1][1];
|
return b[1][1] - a[1][1]
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|
||||||
function compressToBase64(input) {
|
function compressToBase64(input) {
|
||||||
const buffer = gzipSync(input);
|
const buffer = gzipSync(input)
|
||||||
return buffer.toString('base64');
|
return buffer.toString('base64')
|
||||||
}
|
}
|
||||||
|
|
||||||
const filePath = './generated/minecraft-data-optimized.json'
|
const filePath = './generated/minecraft-data-optimized.json'
|
||||||
|
|
@ -330,6 +371,7 @@ console.log('size', fs.lstatSync(filePath).size / 1000 / 1000, gzipSizeFromFileS
|
||||||
|
|
||||||
const { defaultVersion } = MCProtocol
|
const { defaultVersion } = MCProtocol
|
||||||
const data = MinecraftData(defaultVersion)
|
const data = MinecraftData(defaultVersion)
|
||||||
|
console.log('defaultVersion', defaultVersion, !!data)
|
||||||
const initialMcData = {
|
const initialMcData = {
|
||||||
[defaultVersion]: {
|
[defaultVersion]: {
|
||||||
version: data.version,
|
version: data.version,
|
||||||
|
|
|
||||||
137
scripts/patchAssets.ts
Normal file
137
scripts/patchAssets.ts
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
import blocksAtlas from 'mc-assets/dist/blocksAtlases.json'
|
||||||
|
import itemsAtlas from 'mc-assets/dist/itemsAtlases.json'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as path from 'path'
|
||||||
|
import sharp from 'sharp'
|
||||||
|
|
||||||
|
interface AtlasFile {
|
||||||
|
latest: {
|
||||||
|
suSv: number
|
||||||
|
tileSize: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
textures: {
|
||||||
|
[key: string]: {
|
||||||
|
u: number
|
||||||
|
v: number
|
||||||
|
su: number
|
||||||
|
sv: number
|
||||||
|
tileIndex: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function patchTextureAtlas(
|
||||||
|
atlasType: 'blocks' | 'items',
|
||||||
|
atlasData: AtlasFile,
|
||||||
|
customTexturesDir: string,
|
||||||
|
distDir: string
|
||||||
|
) {
|
||||||
|
// Check if custom textures directory exists and has files
|
||||||
|
if (!fs.existsSync(customTexturesDir) || fs.readdirSync(customTexturesDir).length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the latest atlas file
|
||||||
|
const atlasFiles = fs.readdirSync(distDir)
|
||||||
|
.filter(file => file.startsWith(`${atlasType}AtlasLatest`) && file.endsWith('.png'))
|
||||||
|
.sort()
|
||||||
|
|
||||||
|
if (atlasFiles.length === 0) {
|
||||||
|
console.log(`No ${atlasType}AtlasLatest.png found in ${distDir}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestAtlasFile = atlasFiles[atlasFiles.length - 1]
|
||||||
|
const atlasPath = path.join(distDir, latestAtlasFile)
|
||||||
|
console.log(`Patching ${atlasPath}`)
|
||||||
|
|
||||||
|
// Get atlas dimensions
|
||||||
|
const atlasMetadata = await sharp(atlasPath).metadata()
|
||||||
|
if (!atlasMetadata.width || !atlasMetadata.height) {
|
||||||
|
throw new Error(`Failed to get atlas dimensions for ${atlasPath}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process each custom texture
|
||||||
|
const customTextureFiles = fs.readdirSync(customTexturesDir)
|
||||||
|
.filter(file => file.endsWith('.png'))
|
||||||
|
|
||||||
|
if (customTextureFiles.length === 0) return
|
||||||
|
|
||||||
|
// Prepare composite operations
|
||||||
|
const composites: sharp.OverlayOptions[] = []
|
||||||
|
|
||||||
|
for (const textureFile of customTextureFiles) {
|
||||||
|
const textureName = path.basename(textureFile, '.png')
|
||||||
|
|
||||||
|
if (atlasData.latest.textures[textureName]) {
|
||||||
|
const textureData = atlasData.latest.textures[textureName]
|
||||||
|
const customTexturePath = path.join(customTexturesDir, textureFile)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Convert UV coordinates to pixel coordinates
|
||||||
|
const x = Math.round(textureData.u * atlasMetadata.width)
|
||||||
|
const y = Math.round(textureData.v * atlasMetadata.height)
|
||||||
|
const width = Math.round((textureData.su ?? atlasData.latest.suSv) * atlasMetadata.width)
|
||||||
|
const height = Math.round((textureData.sv ?? atlasData.latest.suSv) * atlasMetadata.height)
|
||||||
|
|
||||||
|
// Resize custom texture to match atlas dimensions and add to composite operations
|
||||||
|
const resizedTextureBuffer = await sharp(customTexturePath)
|
||||||
|
.resize(width, height, {
|
||||||
|
fit: 'fill',
|
||||||
|
kernel: 'nearest' // Preserve pixel art quality
|
||||||
|
})
|
||||||
|
.png()
|
||||||
|
.toBuffer()
|
||||||
|
|
||||||
|
composites.push({
|
||||||
|
input: resizedTextureBuffer,
|
||||||
|
left: x,
|
||||||
|
top: y,
|
||||||
|
blend: 'over'
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`Prepared ${textureName} at (${x}, ${y}) with size (${width}, ${height})`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to prepare ${textureName}:`, error)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`Texture ${textureName} not found in ${atlasType} atlas`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (composites.length > 0) {
|
||||||
|
// Apply all patches at once using Sharp's composite
|
||||||
|
await sharp(atlasPath)
|
||||||
|
.composite(composites)
|
||||||
|
.png()
|
||||||
|
.toFile(atlasPath + '.tmp')
|
||||||
|
|
||||||
|
// Replace original with patched version
|
||||||
|
fs.renameSync(atlasPath + '.tmp', atlasPath)
|
||||||
|
console.log(`Saved patched ${atlasType} atlas to ${atlasPath}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const customBlocksDir = './assets/customTextures/blocks'
|
||||||
|
const customItemsDir = './assets/customTextures/items'
|
||||||
|
const distDir = './dist/static/image'
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Patch blocks atlas
|
||||||
|
await patchTextureAtlas('blocks', blocksAtlas as unknown as AtlasFile, customBlocksDir, distDir)
|
||||||
|
|
||||||
|
// Patch items atlas
|
||||||
|
await patchTextureAtlas('items', itemsAtlas as unknown as AtlasFile, customItemsDir, distDir)
|
||||||
|
|
||||||
|
console.log('Texture atlas patching completed!')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to patch texture atlases:', error)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the script
|
||||||
|
main()
|
||||||
42
scripts/requestData.ts
Normal file
42
scripts/requestData.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import WebSocket from 'ws'
|
||||||
|
|
||||||
|
function formatBytes(bytes: number) {
|
||||||
|
return `${(bytes).toFixed(2)} MB`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(ms: number) {
|
||||||
|
return `${(ms / 1000).toFixed(2)}s`
|
||||||
|
}
|
||||||
|
|
||||||
|
const ws = new WebSocket('ws://localhost:8081')
|
||||||
|
|
||||||
|
ws.on('open', () => {
|
||||||
|
console.log('Connected to metrics server, waiting for metrics...')
|
||||||
|
})
|
||||||
|
|
||||||
|
ws.on('message', (data) => {
|
||||||
|
try {
|
||||||
|
const metrics = JSON.parse(data.toString())
|
||||||
|
console.log('\nPerformance Metrics:')
|
||||||
|
console.log('------------------')
|
||||||
|
console.log(`Load Time: ${formatTime(metrics.loadTime)}`)
|
||||||
|
console.log(`Memory Usage: ${formatBytes(metrics.memoryUsage)}`)
|
||||||
|
console.log(`Timestamp: ${new Date(metrics.timestamp).toLocaleString()}`)
|
||||||
|
if (!process.argv.includes('-f')) { // follow mode
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing metrics:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ws.on('error', (error) => {
|
||||||
|
console.error('WebSocket error:', error)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Exit if no metrics received after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
console.error('Timeout waiting for metrics')
|
||||||
|
process.exit(1)
|
||||||
|
}, 5000)
|
||||||
160
scripts/updateGitDeps.ts
Normal file
160
scripts/updateGitDeps.ts
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import yaml from 'yaml'
|
||||||
|
import { execSync } from 'child_process'
|
||||||
|
import { createInterface } from 'readline'
|
||||||
|
|
||||||
|
interface LockfilePackage {
|
||||||
|
specifier: string
|
||||||
|
version: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Lockfile {
|
||||||
|
importers: {
|
||||||
|
'.': {
|
||||||
|
dependencies?: Record<string, LockfilePackage>
|
||||||
|
devDependencies?: Record<string, LockfilePackage>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PackageJson {
|
||||||
|
pnpm?: {
|
||||||
|
updateConfig?: {
|
||||||
|
ignoreDependencies?: string[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prompt(question: string): Promise<string> {
|
||||||
|
const rl = createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
rl.question(question, answer => {
|
||||||
|
rl.close()
|
||||||
|
resolve(answer.toLowerCase().trim())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLatestCommit(owner: string, repo: string): Promise<string> {
|
||||||
|
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/commits/HEAD`)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch latest commit: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
const data = await response.json()
|
||||||
|
return data.sha
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractGitInfo(specifier: string): { owner: string; repo: string; branch: string } | null {
|
||||||
|
const match = specifier.match(/github:([^/]+)\/([^#]+)(?:#(.+))?/)
|
||||||
|
if (!match) return null
|
||||||
|
return {
|
||||||
|
owner: match[1],
|
||||||
|
repo: match[2],
|
||||||
|
branch: match[3] || 'master'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractCommitHash(version: string): string | null {
|
||||||
|
const match = version.match(/https:\/\/codeload\.github\.com\/[^/]+\/[^/]+\/tar\.gz\/([a-f0-9]+)/)
|
||||||
|
return match ? match[1] : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIgnoredDependencies(): string[] {
|
||||||
|
try {
|
||||||
|
const packageJsonPath = path.join(process.cwd(), 'package.json')
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as PackageJson
|
||||||
|
return packageJson.pnpm?.updateConfig?.ignoreDependencies || []
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to read package.json for ignored dependencies:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const lockfilePath = path.join(process.cwd(), 'pnpm-lock.yaml')
|
||||||
|
const lockfileContent = fs.readFileSync(lockfilePath, 'utf8')
|
||||||
|
const lockfile = yaml.parse(lockfileContent) as Lockfile
|
||||||
|
|
||||||
|
const ignoredDependencies = new Set(getIgnoredDependencies())
|
||||||
|
console.log('Ignoring dependencies:', Array.from(ignoredDependencies).join(', ') || 'none')
|
||||||
|
|
||||||
|
const dependencies = {
|
||||||
|
...lockfile.importers['.'].dependencies,
|
||||||
|
...lockfile.importers['.'].devDependencies
|
||||||
|
}
|
||||||
|
|
||||||
|
const updates: Array<{
|
||||||
|
name: string
|
||||||
|
currentHash: string
|
||||||
|
latestHash: string
|
||||||
|
gitInfo: ReturnType<typeof extractGitInfo>
|
||||||
|
}> = []
|
||||||
|
|
||||||
|
console.log('\nChecking git dependencies...')
|
||||||
|
for (const [name, pkg] of Object.entries(dependencies)) {
|
||||||
|
if (ignoredDependencies.has(name)) {
|
||||||
|
console.log(`Skipping ignored dependency: ${name}`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pkg.specifier.startsWith('github:')) continue
|
||||||
|
|
||||||
|
const gitInfo = extractGitInfo(pkg.specifier)
|
||||||
|
if (!gitInfo) continue
|
||||||
|
|
||||||
|
const currentHash = extractCommitHash(pkg.version)
|
||||||
|
if (!currentHash) continue
|
||||||
|
|
||||||
|
try {
|
||||||
|
process.stdout.write(`Checking ${name}... `)
|
||||||
|
const latestHash = await getLatestCommit(gitInfo.owner, gitInfo.repo)
|
||||||
|
if (currentHash !== latestHash) {
|
||||||
|
console.log('update available')
|
||||||
|
updates.push({ name, currentHash, latestHash, gitInfo })
|
||||||
|
} else {
|
||||||
|
console.log('up to date')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('failed')
|
||||||
|
console.error(`Error checking ${name}:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates.length === 0) {
|
||||||
|
console.log('\nAll git dependencies are up to date!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\nThe following git dependencies can be updated:')
|
||||||
|
for (const update of updates) {
|
||||||
|
console.log(`\n${update.name}:`)
|
||||||
|
console.log(` Current: ${update.currentHash}`)
|
||||||
|
console.log(` Latest: ${update.latestHash}`)
|
||||||
|
console.log(` Repo: ${update.gitInfo!.owner}/${update.gitInfo!.repo}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const answer = await prompt('\nWould you like to update these dependencies? (y/N): ')
|
||||||
|
if (answer === 'y' || answer === 'yes') {
|
||||||
|
let newLockfileContent = lockfileContent
|
||||||
|
for (const update of updates) {
|
||||||
|
newLockfileContent = newLockfileContent.replace(
|
||||||
|
new RegExp(update.currentHash, 'g'),
|
||||||
|
update.latestHash
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fs.writeFileSync(lockfilePath, newLockfileContent)
|
||||||
|
console.log('\nUpdated pnpm-lock.yaml with new commit hashes')
|
||||||
|
// console.log('Running pnpm install to apply changes...')
|
||||||
|
// execSync('pnpm install', { stdio: 'inherit' })
|
||||||
|
console.log('Done!')
|
||||||
|
} else {
|
||||||
|
console.log('\nNo changes were made.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error)
|
||||||
45
scripts/wsServer.ts
Normal file
45
scripts/wsServer.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import {WebSocketServer} from 'ws'
|
||||||
|
|
||||||
|
export function startWsServer(port: number = 8081, tryOtherPort: boolean = true): Promise<number> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tryPort = (currentPort: number) => {
|
||||||
|
const wss = new WebSocketServer({ port: currentPort })
|
||||||
|
.on('listening', () => {
|
||||||
|
console.log(`WebSocket server started on port ${currentPort}`)
|
||||||
|
resolve(currentPort)
|
||||||
|
})
|
||||||
|
.on('error', (err: any) => {
|
||||||
|
if (err.code === 'EADDRINUSE' && tryOtherPort) {
|
||||||
|
console.log(`Port ${currentPort} in use, trying ${currentPort + 1}`)
|
||||||
|
wss.close()
|
||||||
|
tryPort(currentPort + 1)
|
||||||
|
} else {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
wss.on('connection', (ws) => {
|
||||||
|
console.log('Client connected')
|
||||||
|
|
||||||
|
ws.on('message', (message) => {
|
||||||
|
try {
|
||||||
|
// Simply relay the message to all connected clients except sender
|
||||||
|
wss.clients.forEach(client => {
|
||||||
|
if (client !== ws && client.readyState === WebSocket.OPEN) {
|
||||||
|
client.send(message.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing message:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ws.on('close', () => {
|
||||||
|
console.log('Client disconnected')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
tryPort(port)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -24,6 +24,7 @@ export type MobileButtonConfig = {
|
||||||
readonly icon?: string
|
readonly icon?: string
|
||||||
readonly action?: ActionType
|
readonly action?: ActionType
|
||||||
readonly actionHold?: ActionType | ActionHoldConfig
|
readonly actionHold?: ActionType | ActionHoldConfig
|
||||||
|
readonly iconStyle?: React.CSSProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AppConfig = {
|
export type AppConfig = {
|
||||||
|
|
@ -34,7 +35,7 @@ export type AppConfig = {
|
||||||
// defaultVersion?: string
|
// defaultVersion?: string
|
||||||
peerJsServer?: string
|
peerJsServer?: string
|
||||||
peerJsServerFallback?: string
|
peerJsServerFallback?: string
|
||||||
promoteServers?: Array<{ ip, description, version? }>
|
promoteServers?: Array<{ ip, description, name?, version?, }>
|
||||||
mapsProvider?: string
|
mapsProvider?: string
|
||||||
|
|
||||||
appParams?: Record<string, any> // query string params
|
appParams?: Record<string, any> // query string params
|
||||||
|
|
@ -54,9 +55,14 @@ export type AppConfig = {
|
||||||
supportedLanguages?: string[]
|
supportedLanguages?: string[]
|
||||||
showModsButton?: boolean
|
showModsButton?: boolean
|
||||||
defaultUsername?: string
|
defaultUsername?: string
|
||||||
|
skinTexturesProxy?: string
|
||||||
|
alwaysReconnectButton?: boolean
|
||||||
|
reportBugButtonWithReconnect?: boolean
|
||||||
|
disabledCommands?: string[] // Array of command IDs to disable (e.g. ['general.jump', 'general.chat'])
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadAppConfig = (appConfig: AppConfig) => {
|
export const loadAppConfig = (appConfig: AppConfig) => {
|
||||||
|
|
||||||
if (miscUiState.appConfig) {
|
if (miscUiState.appConfig) {
|
||||||
Object.assign(miscUiState.appConfig, appConfig)
|
Object.assign(miscUiState.appConfig, appConfig)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -68,7 +74,7 @@ export const loadAppConfig = (appConfig: AppConfig) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
disabledSettings.value.add(key)
|
disabledSettings.value.add(key)
|
||||||
// since the setting is forced, we need to set it to that value
|
// since the setting is forced, we need to set it to that value
|
||||||
if (appConfig.defaultSettings?.[key] && !qsOptions[key]) {
|
if (appConfig.defaultSettings && key in appConfig.defaultSettings && !qsOptions[key]) {
|
||||||
options[key] = appConfig.defaultSettings[key]
|
options[key] = appConfig.defaultSettings[key]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -76,12 +82,15 @@ export const loadAppConfig = (appConfig: AppConfig) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// todo apply defaultSettings to defaults even if not forced in case of remote config
|
||||||
|
|
||||||
if (appConfig.keybindings) {
|
if (appConfig.keybindings) {
|
||||||
Object.assign(customKeymaps, defaultsDeep(appConfig.keybindings, customKeymaps))
|
Object.assign(customKeymaps, defaultsDeep(appConfig.keybindings, customKeymaps))
|
||||||
updateBinds(customKeymaps)
|
updateBinds(customKeymaps)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appViewer?.appConfigUdpate()
|
||||||
|
|
||||||
setStorageDataOnAppConfigLoad(appConfig)
|
setStorageDataOnAppConfigLoad(appConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export type AppQsParams = {
|
||||||
username?: string
|
username?: string
|
||||||
lockConnect?: string
|
lockConnect?: string
|
||||||
autoConnect?: string
|
autoConnect?: string
|
||||||
|
alwaysReconnect?: string
|
||||||
// googledrive.ts params
|
// googledrive.ts params
|
||||||
state?: string
|
state?: string
|
||||||
// ServersListProvider.tsx params
|
// ServersListProvider.tsx params
|
||||||
|
|
@ -45,6 +46,8 @@ export type AppQsParams = {
|
||||||
onlyConnect?: string
|
onlyConnect?: string
|
||||||
connectText?: string
|
connectText?: string
|
||||||
freezeSettings?: string
|
freezeSettings?: string
|
||||||
|
testIosCrash?: string
|
||||||
|
addPing?: string
|
||||||
|
|
||||||
// Replay params
|
// Replay params
|
||||||
replayFilter?: string
|
replayFilter?: string
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { resetStateAfterDisconnect } from './browserfs'
|
||||||
import { hideModal, activeModalStack, showModal, miscUiState } from './globalState'
|
import { hideModal, activeModalStack, showModal, miscUiState } from './globalState'
|
||||||
import { appStatusState, resetAppStatusState } from './react/AppStatusProvider'
|
import { appStatusState, resetAppStatusState } from './react/AppStatusProvider'
|
||||||
|
|
||||||
|
|
@ -25,7 +26,6 @@ export const setLoadingScreenStatus = function (status: string | undefined | nul
|
||||||
}
|
}
|
||||||
showModal({ reactType: 'app-status' })
|
showModal({ reactType: 'app-status' })
|
||||||
if (appStatusState.isError) {
|
if (appStatusState.isError) {
|
||||||
miscUiState.gameLoaded = false
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
appStatusState.hideDots = hideDots
|
appStatusState.hideDots = hideDots
|
||||||
|
|
@ -33,5 +33,9 @@ export const setLoadingScreenStatus = function (status: string | undefined | nul
|
||||||
appStatusState.lastStatus = isError ? appStatusState.status : ''
|
appStatusState.lastStatus = isError ? appStatusState.status : ''
|
||||||
appStatusState.status = status
|
appStatusState.status = status
|
||||||
appStatusState.minecraftJsonMessage = minecraftJsonMessage ?? null
|
appStatusState.minecraftJsonMessage = minecraftJsonMessage ?? null
|
||||||
|
|
||||||
|
if (isError && miscUiState.gameLoaded) {
|
||||||
|
resetStateAfterDisconnect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
globalThis.setLoadingScreenStatus = setLoadingScreenStatus
|
globalThis.setLoadingScreenStatus = setLoadingScreenStatus
|
||||||
|
|
|
||||||
161
src/appViewer.ts
161
src/appViewer.ts
|
|
@ -1,25 +1,30 @@
|
||||||
import { WorldDataEmitter } from 'renderer/viewer/lib/worldDataEmitter'
|
import { WorldDataEmitter, WorldDataEmitterWorker } from 'renderer/viewer/lib/worldDataEmitter'
|
||||||
import { BasePlayerState, IPlayerState } from 'renderer/viewer/lib/basePlayerState'
|
import { getInitialPlayerState, PlayerStateRenderer, PlayerStateReactive } from 'renderer/viewer/lib/basePlayerState'
|
||||||
import { subscribeKey } from 'valtio/utils'
|
import { subscribeKey } from 'valtio/utils'
|
||||||
import { defaultWorldRendererConfig, WorldRendererConfig } from 'renderer/viewer/lib/worldrendererCommon'
|
import { defaultWorldRendererConfig, WorldRendererConfig } from 'renderer/viewer/lib/worldrendererCommon'
|
||||||
import { Vec3 } from 'vec3'
|
import { Vec3 } from 'vec3'
|
||||||
import { SoundSystem } from 'renderer/viewer/three/threeJsSound'
|
import { SoundSystem } from 'renderer/viewer/three/threeJsSound'
|
||||||
import { proxy } from 'valtio'
|
import { proxy, subscribe } from 'valtio'
|
||||||
import { getDefaultRendererState } from 'renderer/viewer/baseGraphicsBackend'
|
import { getDefaultRendererState } from 'renderer/viewer/baseGraphicsBackend'
|
||||||
import { getSyncWorld } from 'renderer/playground/shared'
|
import { getSyncWorld } from 'renderer/playground/shared'
|
||||||
|
import { MaybePromise } from 'contro-max/build/types/store'
|
||||||
|
import { PANORAMA_VERSION } from 'renderer/viewer/three/panoramaShared'
|
||||||
import { playerState } from './mineflayer/playerState'
|
import { playerState } from './mineflayer/playerState'
|
||||||
import { createNotificationProgressReporter, ProgressReporter } from './core/progressReporter'
|
import { createNotificationProgressReporter, ProgressReporter } from './core/progressReporter'
|
||||||
import { setLoadingScreenStatus } from './appStatus'
|
import { setLoadingScreenStatus } from './appStatus'
|
||||||
import { activeModalStack, miscUiState } from './globalState'
|
import { activeModalStack, miscUiState } from './globalState'
|
||||||
import { options } from './optionsStorage'
|
import { options } from './optionsStorage'
|
||||||
import { ResourcesManager } from './resourcesManager'
|
import { ResourcesManager, ResourcesManagerTransferred } from './resourcesManager'
|
||||||
import { watchOptionsAfterWorldViewInit } from './watchOptions'
|
import { watchOptionsAfterWorldViewInit } from './watchOptions'
|
||||||
|
import { loadMinecraftData } from './connect'
|
||||||
|
import { reloadChunks } from './utils'
|
||||||
|
import { displayClientChat } from './botUtils'
|
||||||
|
|
||||||
export interface RendererReactiveState {
|
export interface RendererReactiveState {
|
||||||
world: {
|
world: {
|
||||||
chunksLoaded: Set<string>
|
chunksLoaded: Set<string>
|
||||||
|
// chunksTotalNumber: number
|
||||||
heightmaps: Map<string, Uint8Array>
|
heightmaps: Map<string, Uint8Array>
|
||||||
chunksTotalNumber: number
|
|
||||||
allChunksLoaded: boolean
|
allChunksLoaded: boolean
|
||||||
mesherWork: boolean
|
mesherWork: boolean
|
||||||
intersectMedia: { id: string, x: number, y: number } | null
|
intersectMedia: { id: string, x: number, y: number } | null
|
||||||
|
|
@ -31,9 +36,6 @@ export interface NonReactiveState {
|
||||||
world: {
|
world: {
|
||||||
chunksLoaded: Set<string>
|
chunksLoaded: Set<string>
|
||||||
chunksTotalNumber: number
|
chunksTotalNumber: number
|
||||||
allChunksLoaded: boolean
|
|
||||||
mesherWork: boolean
|
|
||||||
intersectMedia: { id: string, x: number, y: number } | null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,33 +44,39 @@ export interface GraphicsBackendConfig {
|
||||||
powerPreference?: 'high-performance' | 'low-power'
|
powerPreference?: 'high-performance' | 'low-power'
|
||||||
statsVisible?: number
|
statsVisible?: number
|
||||||
sceneBackground: string
|
sceneBackground: string
|
||||||
|
timeoutRendering?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultGraphicsBackendConfig: GraphicsBackendConfig = {
|
const defaultGraphicsBackendConfig: GraphicsBackendConfig = {
|
||||||
fpsLimit: undefined,
|
fpsLimit: undefined,
|
||||||
powerPreference: undefined,
|
powerPreference: undefined,
|
||||||
sceneBackground: 'lightblue'
|
sceneBackground: 'lightblue',
|
||||||
|
timeoutRendering: false
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GraphicsInitOptions<S = any> {
|
export interface GraphicsInitOptions<S = any> {
|
||||||
resourcesManager: ResourcesManager
|
resourcesManager: ResourcesManagerTransferred
|
||||||
config: GraphicsBackendConfig
|
config: GraphicsBackendConfig
|
||||||
rendererSpecificSettings: S
|
rendererSpecificSettings: S
|
||||||
|
|
||||||
displayCriticalError: (error: Error) => void
|
callbacks: {
|
||||||
setRendererSpecificSettings: (key: string, value: any) => void
|
displayCriticalError: (error: Error) => void
|
||||||
|
setRendererSpecificSettings: (key: string, value: any) => void
|
||||||
|
|
||||||
|
fireCustomEvent: (eventName: string, ...args: any[]) => void
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DisplayWorldOptions {
|
export interface DisplayWorldOptions {
|
||||||
version: string
|
version: string
|
||||||
worldView: WorldDataEmitter
|
worldView: WorldDataEmitterWorker
|
||||||
inWorldRenderingConfig: WorldRendererConfig
|
inWorldRenderingConfig: WorldRendererConfig
|
||||||
playerState: IPlayerState
|
playerStateReactive: PlayerStateReactive
|
||||||
rendererState: RendererReactiveState
|
rendererState: RendererReactiveState
|
||||||
nonReactiveState: NonReactiveState
|
nonReactiveState: NonReactiveState
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GraphicsBackendLoader = ((options: GraphicsInitOptions) => GraphicsBackend) & {
|
export type GraphicsBackendLoader = ((options: GraphicsInitOptions) => MaybePromise<GraphicsBackend>) & {
|
||||||
id: string
|
id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,8 +116,8 @@ export class AppViewer {
|
||||||
inWorldRenderingConfig: WorldRendererConfig = proxy(defaultWorldRendererConfig)
|
inWorldRenderingConfig: WorldRendererConfig = proxy(defaultWorldRendererConfig)
|
||||||
lastCamUpdate = 0
|
lastCamUpdate = 0
|
||||||
playerState = playerState
|
playerState = playerState
|
||||||
rendererState = proxy(getDefaultRendererState())
|
rendererState = getDefaultRendererState().reactive
|
||||||
nonReactiveState: NonReactiveState = getDefaultRendererState()
|
nonReactiveState: NonReactiveState = getDefaultRendererState().nonReactive
|
||||||
worldReady: Promise<void>
|
worldReady: Promise<void>
|
||||||
private resolveWorldReady: () => void
|
private resolveWorldReady: () => void
|
||||||
|
|
||||||
|
|
@ -133,19 +141,24 @@ export class AppViewer {
|
||||||
rendererSpecificSettings[key.slice(rendererSettingsKey.length + 1)] = options[key]
|
rendererSpecificSettings[key.slice(rendererSettingsKey.length + 1)] = options[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const loaderOptions: GraphicsInitOptions = {
|
const loaderOptions: GraphicsInitOptions = { // todo!
|
||||||
resourcesManager: this.resourcesManager,
|
resourcesManager: this.resourcesManager as ResourcesManagerTransferred,
|
||||||
config: this.config,
|
config: this.config,
|
||||||
displayCriticalError (error) {
|
callbacks: {
|
||||||
console.error(error)
|
displayCriticalError (error) {
|
||||||
setLoadingScreenStatus(error.message, true)
|
console.error(error)
|
||||||
|
setLoadingScreenStatus(error.message, true)
|
||||||
|
},
|
||||||
|
setRendererSpecificSettings (key: string, value: any) {
|
||||||
|
options[`${rendererSettingsKey}.${key}`] = value
|
||||||
|
},
|
||||||
|
fireCustomEvent (eventName, ...args) {
|
||||||
|
// this.callbacks.fireCustomEvent(eventName, ...args)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
rendererSpecificSettings,
|
rendererSpecificSettings,
|
||||||
setRendererSpecificSettings (key: string, value: any) {
|
|
||||||
options[`${rendererSettingsKey}.${key}`] = value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.backend = loader(loaderOptions)
|
this.backend = await loader(loaderOptions)
|
||||||
|
|
||||||
// if (this.resourcesManager.currentResources) {
|
// if (this.resourcesManager.currentResources) {
|
||||||
// void this.prepareResources(this.resourcesManager.currentResources.version, createNotificationProgressReporter())
|
// void this.prepareResources(this.resourcesManager.currentResources.version, createNotificationProgressReporter())
|
||||||
|
|
@ -153,12 +166,20 @@ export class AppViewer {
|
||||||
|
|
||||||
// Execute queued action if exists
|
// Execute queued action if exists
|
||||||
if (this.currentState) {
|
if (this.currentState) {
|
||||||
const { method, args } = this.currentState
|
if (this.currentState.method === 'startPanorama') {
|
||||||
this.backend[method](...args)
|
this.startPanorama()
|
||||||
if (method === 'startWorld') {
|
} else {
|
||||||
// void this.worldView!.init(args[0].playerState.getPosition())
|
const { method, args } = this.currentState
|
||||||
|
this.backend[method](...args)
|
||||||
|
if (method === 'startWorld') {
|
||||||
|
void this.worldView!.init(bot.entity.position)
|
||||||
|
// void this.worldView!.init(args[0].playerState.getPosition())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo
|
||||||
|
modalStackUpdateChecks()
|
||||||
}
|
}
|
||||||
|
|
||||||
async startWithBot () {
|
async startWithBot () {
|
||||||
|
|
@ -167,19 +188,33 @@ export class AppViewer {
|
||||||
this.worldView!.listenToBot(bot)
|
this.worldView!.listenToBot(bot)
|
||||||
}
|
}
|
||||||
|
|
||||||
async startWorld (world, renderDistance: number, playerStateSend: IPlayerState = this.playerState) {
|
appConfigUdpate () {
|
||||||
|
if (miscUiState.appConfig) {
|
||||||
|
this.inWorldRenderingConfig.skinTexturesProxy = miscUiState.appConfig.skinTexturesProxy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async startWorld (world, renderDistance: number, playerStateSend: PlayerStateRenderer = this.playerState.reactive) {
|
||||||
if (this.currentDisplay === 'world') throw new Error('World already started')
|
if (this.currentDisplay === 'world') throw new Error('World already started')
|
||||||
this.currentDisplay = 'world'
|
this.currentDisplay = 'world'
|
||||||
const startPosition = playerStateSend.getPosition()
|
const startPosition = bot.entity?.position ?? new Vec3(0, 64, 0)
|
||||||
this.worldView = new WorldDataEmitter(world, renderDistance, startPosition)
|
this.worldView = new WorldDataEmitter(world, renderDistance, startPosition)
|
||||||
|
this.worldView.panicChunksReload = () => {
|
||||||
|
if (!options.experimentalClientSelfReload) return
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
displayClientChat(`[client] client panicked due to too long loading time. Soft reloading chunks...`)
|
||||||
|
}
|
||||||
|
void reloadChunks()
|
||||||
|
}
|
||||||
window.worldView = this.worldView
|
window.worldView = this.worldView
|
||||||
watchOptionsAfterWorldViewInit(this.worldView)
|
watchOptionsAfterWorldViewInit(this.worldView)
|
||||||
|
this.appConfigUdpate()
|
||||||
|
|
||||||
const displayWorldOptions: DisplayWorldOptions = {
|
const displayWorldOptions: DisplayWorldOptions = {
|
||||||
version: this.resourcesManager.currentConfig!.version,
|
version: this.resourcesManager.currentConfig!.version,
|
||||||
worldView: this.worldView,
|
worldView: this.worldView,
|
||||||
inWorldRenderingConfig: this.inWorldRenderingConfig,
|
inWorldRenderingConfig: this.inWorldRenderingConfig,
|
||||||
playerState: playerStateSend,
|
playerStateReactive: playerStateSend,
|
||||||
rendererState: this.rendererState,
|
rendererState: this.rendererState,
|
||||||
nonReactiveState: this.nonReactiveState
|
nonReactiveState: this.nonReactiveState
|
||||||
}
|
}
|
||||||
|
|
@ -205,10 +240,16 @@ export class AppViewer {
|
||||||
|
|
||||||
startPanorama () {
|
startPanorama () {
|
||||||
if (this.currentDisplay === 'menu') return
|
if (this.currentDisplay === 'menu') return
|
||||||
this.currentDisplay = 'menu'
|
|
||||||
if (options.disableAssets) return
|
if (options.disableAssets) return
|
||||||
if (this.backend) {
|
if (this.backend && !hasAppStatus()) {
|
||||||
this.backend.startPanorama()
|
this.currentDisplay = 'menu'
|
||||||
|
if (process.env.SINGLE_FILE_BUILD_MODE) {
|
||||||
|
void loadMinecraftData(PANORAMA_VERSION).then(() => {
|
||||||
|
this.backend?.startPanorama()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.backend.startPanorama()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.currentState = { method: 'startPanorama', args: [] }
|
this.currentState = { method: 'startPanorama', args: [] }
|
||||||
}
|
}
|
||||||
|
|
@ -238,7 +279,8 @@ export class AppViewer {
|
||||||
const { promise, resolve } = Promise.withResolvers<void>()
|
const { promise, resolve } = Promise.withResolvers<void>()
|
||||||
this.worldReady = promise
|
this.worldReady = promise
|
||||||
this.resolveWorldReady = resolve
|
this.resolveWorldReady = resolve
|
||||||
this.rendererState = proxy(getDefaultRendererState())
|
this.rendererState = proxy(getDefaultRendererState().reactive)
|
||||||
|
this.nonReactiveState = getDefaultRendererState().nonReactive
|
||||||
// this.queuedDisplay = undefined
|
// this.queuedDisplay = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -259,6 +301,7 @@ export class AppViewer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// do not import this. Use global appViewer instead (without window prefix).
|
||||||
export const appViewer = new AppViewer()
|
export const appViewer = new AppViewer()
|
||||||
window.appViewer = appViewer
|
window.appViewer = appViewer
|
||||||
|
|
||||||
|
|
@ -266,34 +309,46 @@ const initialMenuStart = async () => {
|
||||||
if (appViewer.currentDisplay === 'world') {
|
if (appViewer.currentDisplay === 'world') {
|
||||||
appViewer.resetBackend(true)
|
appViewer.resetBackend(true)
|
||||||
}
|
}
|
||||||
appViewer.startPanorama()
|
const demo = new URLSearchParams(window.location.search).get('demo')
|
||||||
|
if (!demo) {
|
||||||
|
appViewer.startPanorama()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// const version = '1.18.2'
|
// const version = '1.18.2'
|
||||||
// const version = '1.21.4'
|
const version = '1.21.4'
|
||||||
// await appViewer.resourcesManager.loadMcData(version)
|
const { loadMinecraftData } = await import('./connect')
|
||||||
// const world = getSyncWorld(version)
|
const { getSyncWorld } = await import('../renderer/playground/shared')
|
||||||
// world.setBlockStateId(new Vec3(0, 64, 0), loadedData.blocksByName.water.defaultState)
|
await loadMinecraftData(version)
|
||||||
// appViewer.resourcesManager.currentConfig = { version }
|
const world = getSyncWorld(version)
|
||||||
// await appViewer.resourcesManager.updateAssetsData({})
|
world.setBlockStateId(new Vec3(0, 64, 0), loadedData.blocksByName.water.defaultState)
|
||||||
// appViewer.playerState = new BasePlayerState() as any
|
world.setBlockStateId(new Vec3(1, 64, 0), loadedData.blocksByName.water.defaultState)
|
||||||
// await appViewer.startWorld(world, 3)
|
world.setBlockStateId(new Vec3(1, 64, 1), loadedData.blocksByName.water.defaultState)
|
||||||
// appViewer.backend?.updateCamera(new Vec3(0, 64, 2), 0, 0)
|
world.setBlockStateId(new Vec3(0, 64, 1), loadedData.blocksByName.water.defaultState)
|
||||||
// void appViewer.worldView!.init(new Vec3(0, 64, 0))
|
world.setBlockStateId(new Vec3(-1, 64, -1), loadedData.blocksByName.water.defaultState)
|
||||||
|
world.setBlockStateId(new Vec3(-1, 64, 0), loadedData.blocksByName.water.defaultState)
|
||||||
|
world.setBlockStateId(new Vec3(0, 64, -1), loadedData.blocksByName.water.defaultState)
|
||||||
|
appViewer.resourcesManager.currentConfig = { version }
|
||||||
|
appViewer.playerState.reactive = getInitialPlayerState()
|
||||||
|
await appViewer.resourcesManager.updateAssetsData({})
|
||||||
|
await appViewer.startWorld(world, 3)
|
||||||
|
appViewer.backend!.updateCamera(new Vec3(0, 65.7, 0), 0, -Math.PI / 2) // Y+1 and pitch = PI/2 to look down
|
||||||
|
void appViewer.worldView!.init(new Vec3(0, 64, 0))
|
||||||
}
|
}
|
||||||
window.initialMenuStart = initialMenuStart
|
window.initialMenuStart = initialMenuStart
|
||||||
|
|
||||||
|
const hasAppStatus = () => activeModalStack.some(m => m.reactType === 'app-status')
|
||||||
|
|
||||||
const modalStackUpdateChecks = () => {
|
const modalStackUpdateChecks = () => {
|
||||||
// maybe start panorama
|
// maybe start panorama
|
||||||
if (activeModalStack.length === 0 && !miscUiState.gameLoaded) {
|
if (!miscUiState.gameLoaded && !hasAppStatus()) {
|
||||||
void initialMenuStart()
|
void initialMenuStart()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appViewer.backend) {
|
if (appViewer.backend) {
|
||||||
const hasAppStatus = activeModalStack.some(m => m.reactType === 'app-status')
|
appViewer.backend.setRendering(!hasAppStatus())
|
||||||
appViewer.backend.setRendering(!hasAppStatus)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
appViewer.inWorldRenderingConfig.foreground = activeModalStack.length === 0
|
appViewer.inWorldRenderingConfig.foreground = activeModalStack.length === 0
|
||||||
}
|
}
|
||||||
subscribeKey(activeModalStack, 'length', modalStackUpdateChecks)
|
subscribe(activeModalStack, modalStackUpdateChecks)
|
||||||
modalStackUpdateChecks()
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,12 @@ let audioContext: AudioContext
|
||||||
const sounds: Record<string, any> = {}
|
const sounds: Record<string, any> = {}
|
||||||
|
|
||||||
// Track currently playing sounds and their gain nodes
|
// Track currently playing sounds and their gain nodes
|
||||||
const activeSounds: Array<{ source: AudioBufferSourceNode; gainNode: GainNode; volumeMultiplier: number }> = []
|
const activeSounds: Array<{
|
||||||
|
source: AudioBufferSourceNode;
|
||||||
|
gainNode: GainNode;
|
||||||
|
volumeMultiplier: number;
|
||||||
|
isMusic: boolean;
|
||||||
|
}> = []
|
||||||
window.activeSounds = activeSounds
|
window.activeSounds = activeSounds
|
||||||
|
|
||||||
// load as many resources on page load as possible instead on demand as user can disable internet connection after he thinks the page is loaded
|
// load as many resources on page load as possible instead on demand as user can disable internet connection after he thinks the page is loaded
|
||||||
|
|
@ -43,7 +48,7 @@ export async function loadSound (path: string, contents = path) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = 500) => {
|
export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = options.remoteSoundsLoadTimeout, loop = false, isMusic = false) => {
|
||||||
const soundBuffer = sounds[url]
|
const soundBuffer = sounds[url]
|
||||||
if (!soundBuffer) {
|
if (!soundBuffer) {
|
||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
|
|
@ -51,11 +56,11 @@ export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = 500) =
|
||||||
if (cancelled || Date.now() - start > loadTimeout) return
|
if (cancelled || Date.now() - start > loadTimeout) return
|
||||||
}
|
}
|
||||||
|
|
||||||
return playSound(url, soundVolume)
|
return playSound(url, soundVolume, loop, isMusic)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function playSound (url, soundVolume = 1) {
|
export async function playSound (url, soundVolume = 1, loop = false, isMusic = false) {
|
||||||
const volume = soundVolume * (options.volume / 100)
|
const volume = soundVolume * (options.volume / 100) * (isMusic ? options.musicVolume / 100 : 1)
|
||||||
|
|
||||||
if (!volume) return
|
if (!volume) return
|
||||||
|
|
||||||
|
|
@ -75,13 +80,14 @@ export async function playSound (url, soundVolume = 1) {
|
||||||
const gainNode = audioContext.createGain()
|
const gainNode = audioContext.createGain()
|
||||||
const source = audioContext.createBufferSource()
|
const source = audioContext.createBufferSource()
|
||||||
source.buffer = soundBuffer
|
source.buffer = soundBuffer
|
||||||
|
source.loop = loop
|
||||||
source.connect(gainNode)
|
source.connect(gainNode)
|
||||||
gainNode.connect(audioContext.destination)
|
gainNode.connect(audioContext.destination)
|
||||||
gainNode.gain.value = volume
|
gainNode.gain.value = volume
|
||||||
source.start(0)
|
source.start(0)
|
||||||
|
|
||||||
// Add to active sounds
|
// Add to active sounds
|
||||||
activeSounds.push({ source, gainNode, volumeMultiplier: soundVolume })
|
activeSounds.push({ source, gainNode, volumeMultiplier: soundVolume, isMusic })
|
||||||
|
|
||||||
const callbacks = [] as Array<() => void>
|
const callbacks = [] as Array<() => void>
|
||||||
source.onended = () => {
|
source.onended = () => {
|
||||||
|
|
@ -99,6 +105,17 @@ export async function playSound (url, soundVolume = 1) {
|
||||||
onEnded (callback: () => void) {
|
onEnded (callback: () => void) {
|
||||||
callbacks.push(callback)
|
callbacks.push(callback)
|
||||||
},
|
},
|
||||||
|
stop () {
|
||||||
|
try {
|
||||||
|
source.stop()
|
||||||
|
// Remove from active sounds
|
||||||
|
const index = activeSounds.findIndex(s => s.source === source)
|
||||||
|
if (index !== -1) activeSounds.splice(index, 1)
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to stop sound:', err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
gainNode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,11 +130,24 @@ export function stopAllSounds () {
|
||||||
activeSounds.length = 0
|
activeSounds.length = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
export function changeVolumeOfCurrentlyPlayingSounds (newVolume: number) {
|
export function stopSound (url: string) {
|
||||||
const normalizedVolume = newVolume / 100
|
const soundIndex = activeSounds.findIndex(s => s.source.buffer === sounds[url])
|
||||||
for (const { gainNode, volumeMultiplier } of activeSounds) {
|
if (soundIndex !== -1) {
|
||||||
|
const { source } = activeSounds[soundIndex]
|
||||||
try {
|
try {
|
||||||
gainNode.gain.value = normalizedVolume * volumeMultiplier
|
source.stop()
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to stop sound:', err)
|
||||||
|
}
|
||||||
|
activeSounds.splice(soundIndex, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function changeVolumeOfCurrentlyPlayingSounds (newVolume: number, newMusicVolume: number) {
|
||||||
|
const normalizedVolume = newVolume / 100
|
||||||
|
for (const { gainNode, volumeMultiplier, isMusic } of activeSounds) {
|
||||||
|
try {
|
||||||
|
gainNode.gain.value = normalizedVolume * volumeMultiplier * (isMusic ? newMusicVolume / 100 : 1)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Failed to change sound volume:', err)
|
console.warn('Failed to change sound volume:', err)
|
||||||
}
|
}
|
||||||
|
|
@ -125,5 +155,9 @@ export function changeVolumeOfCurrentlyPlayingSounds (newVolume: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribeKey(options, 'volume', () => {
|
subscribeKey(options, 'volume', () => {
|
||||||
changeVolumeOfCurrentlyPlayingSounds(options.volume)
|
changeVolumeOfCurrentlyPlayingSounds(options.volume, options.musicVolume)
|
||||||
|
})
|
||||||
|
|
||||||
|
subscribeKey(options, 'musicVolume', () => {
|
||||||
|
changeVolumeOfCurrentlyPlayingSounds(options.volume, options.musicVolume)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -263,7 +263,7 @@ export const mountGoogleDriveFolder = async (readonly: boolean, rootId: string)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeFileRecursiveAsync (path) {
|
export async function removeFileRecursiveAsync (path, removeDirectoryItself = true) {
|
||||||
const errors = [] as Array<[string, Error]>
|
const errors = [] as Array<[string, Error]>
|
||||||
try {
|
try {
|
||||||
const files = await fs.promises.readdir(path)
|
const files = await fs.promises.readdir(path)
|
||||||
|
|
@ -282,7 +282,9 @@ export async function removeFileRecursiveAsync (path) {
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// After removing all files/directories, remove the current directory
|
// After removing all files/directories, remove the current directory
|
||||||
await fs.promises.rmdir(path)
|
if (removeDirectoryItself) {
|
||||||
|
await fs.promises.rmdir(path)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errors.push([path, error])
|
errors.push([path, error])
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ export function onCameraMove (e: MouseEvent | CameraMoveEvent) {
|
||||||
if (!isGameActive(true)) return
|
if (!isGameActive(true)) return
|
||||||
if (e.type === 'mousemove' && !document.pointerLockElement) return
|
if (e.type === 'mousemove' && !document.pointerLockElement) return
|
||||||
e.stopPropagation?.()
|
e.stopPropagation?.()
|
||||||
|
if (appViewer.playerState.utils.isSpectatingEntity()) return
|
||||||
const now = performance.now()
|
const now = performance.now()
|
||||||
// todo: limit camera movement for now to avoid unexpected jumps
|
// todo: limit camera movement for now to avoid unexpected jumps
|
||||||
if (now - lastMouseMove < 4 && !options.preciseMouseInput) return
|
if (now - lastMouseMove < 4 && !options.preciseMouseInput) return
|
||||||
|
|
@ -32,7 +33,6 @@ export function onCameraMove (e: MouseEvent | CameraMoveEvent) {
|
||||||
updateMotion()
|
updateMotion()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const moveCameraRawHandler = ({ x, y }: { x: number; y: number }) => {
|
export const moveCameraRawHandler = ({ x, y }: { x: number; y: number }) => {
|
||||||
const maxPitch = 0.5 * Math.PI
|
const maxPitch = 0.5 * Math.PI
|
||||||
const minPitch = -0.5 * Math.PI
|
const minPitch = -0.5 * Math.PI
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,10 @@ import { fromFormattedString, TextComponent } from '@xmcl/text-component'
|
||||||
import type { IndexedData } from 'minecraft-data'
|
import type { IndexedData } from 'minecraft-data'
|
||||||
import { versionToNumber } from 'renderer/viewer/common/utils'
|
import { versionToNumber } from 'renderer/viewer/common/utils'
|
||||||
|
|
||||||
|
export interface MessageFormatOptions {
|
||||||
|
doShadow?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export type MessageFormatPart = Pick<TextComponent, 'hoverEvent' | 'clickEvent'> & {
|
export type MessageFormatPart = Pick<TextComponent, 'hoverEvent' | 'clickEvent'> & {
|
||||||
text: string
|
text: string
|
||||||
color?: string
|
color?: string
|
||||||
|
|
@ -114,6 +118,14 @@ export const formatMessage = (message: MessageInput, mcData: IndexedData = globa
|
||||||
return msglist
|
return msglist
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const messageToString = (message: MessageInput | string) => {
|
||||||
|
if (typeof message === 'string') {
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
const msglist = formatMessage(message)
|
||||||
|
return msglist.map(msg => msg.text).join('')
|
||||||
|
}
|
||||||
|
|
||||||
const blockToItemRemaps = {
|
const blockToItemRemaps = {
|
||||||
water: 'water_bucket',
|
water: 'water_bucket',
|
||||||
lava: 'lava_bucket',
|
lava: 'lava_bucket',
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
import MinecraftData from 'minecraft-data'
|
import MinecraftData from 'minecraft-data'
|
||||||
import PrismarineBlock from 'prismarine-block'
|
import PrismarineBlock from 'prismarine-block'
|
||||||
import PrismarineItem from 'prismarine-item'
|
import PrismarineItem from 'prismarine-item'
|
||||||
import pathfinder from 'mineflayer-pathfinder'
|
|
||||||
import { miscUiState } from './globalState'
|
import { miscUiState } from './globalState'
|
||||||
import supportedVersions from './supportedVersions.mjs'
|
import supportedVersions from './supportedVersions.mjs'
|
||||||
import { options } from './optionsStorage'
|
import { options } from './optionsStorage'
|
||||||
|
|
@ -65,7 +64,6 @@ export const loadMinecraftData = async (version: string) => {
|
||||||
window.PrismarineItem = PrismarineItem(mcData.version.minecraftVersion!)
|
window.PrismarineItem = PrismarineItem(mcData.version.minecraftVersion!)
|
||||||
window.loadedData = mcData
|
window.loadedData = mcData
|
||||||
window.mcData = mcData
|
window.mcData = mcData
|
||||||
window.pathfinder = pathfinder
|
|
||||||
miscUiState.loadedDataVersion = version
|
miscUiState.loadedDataVersion = version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import { appStorage } from './react/appStorageProvider'
|
||||||
import { switchGameMode } from './packetsReplay/replayPackets'
|
import { switchGameMode } from './packetsReplay/replayPackets'
|
||||||
import { tabListState } from './react/PlayerListOverlayProvider'
|
import { tabListState } from './react/PlayerListOverlayProvider'
|
||||||
import { type ActionType, type ActionHoldConfig, type CustomAction } from './appConfig'
|
import { type ActionType, type ActionHoldConfig, type CustomAction } from './appConfig'
|
||||||
|
import { playerState } from './mineflayer/playerState'
|
||||||
|
|
||||||
export const customKeymaps = proxy(appStorage.keybindings)
|
export const customKeymaps = proxy(appStorage.keybindings)
|
||||||
subscribe(customKeymaps, () => {
|
subscribe(customKeymaps, () => {
|
||||||
|
|
@ -70,6 +71,7 @@ export const contro = new ControMax({
|
||||||
// client side
|
// client side
|
||||||
zoom: ['KeyC'],
|
zoom: ['KeyC'],
|
||||||
viewerConsole: ['Backquote'],
|
viewerConsole: ['Backquote'],
|
||||||
|
togglePerspective: ['F5'],
|
||||||
},
|
},
|
||||||
ui: {
|
ui: {
|
||||||
toggleFullscreen: ['F11'],
|
toggleFullscreen: ['F11'],
|
||||||
|
|
@ -114,6 +116,10 @@ export const contro = new ControMax({
|
||||||
window.controMax = contro
|
window.controMax = contro
|
||||||
export type Command = CommandEventArgument<typeof contro['_commandsRaw']>['command']
|
export type Command = CommandEventArgument<typeof contro['_commandsRaw']>['command']
|
||||||
|
|
||||||
|
export const isCommandDisabled = (command: Command) => {
|
||||||
|
return miscUiState.appConfig?.disabledCommands?.includes(command)
|
||||||
|
}
|
||||||
|
|
||||||
onControInit()
|
onControInit()
|
||||||
|
|
||||||
updateBinds(customKeymaps)
|
updateBinds(customKeymaps)
|
||||||
|
|
@ -131,7 +137,14 @@ const setSprinting = (state: boolean) => {
|
||||||
gameAdditionalState.isSprinting = state
|
gameAdditionalState.isSprinting = state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isSpectatingEntity = () => {
|
||||||
|
return appViewer.playerState.utils.isSpectatingEntity()
|
||||||
|
}
|
||||||
|
|
||||||
contro.on('movementUpdate', ({ vector, soleVector, gamepadIndex }) => {
|
contro.on('movementUpdate', ({ vector, soleVector, gamepadIndex }) => {
|
||||||
|
// Don't allow movement while spectating an entity
|
||||||
|
if (isSpectatingEntity()) return
|
||||||
|
|
||||||
if (gamepadIndex !== undefined && gamepadUiCursorState.display) {
|
if (gamepadIndex !== undefined && gamepadUiCursorState.display) {
|
||||||
const deadzone = 0.1 // TODO make deadzone configurable
|
const deadzone = 0.1 // TODO make deadzone configurable
|
||||||
if (Math.abs(soleVector.x) < deadzone && Math.abs(soleVector.z) < deadzone) {
|
if (Math.abs(soleVector.x) < deadzone && Math.abs(soleVector.z) < deadzone) {
|
||||||
|
|
@ -340,6 +353,9 @@ const cameraRotationControls = {
|
||||||
cameraRotationControls.updateMovement()
|
cameraRotationControls.updateMovement()
|
||||||
},
|
},
|
||||||
handleCommand (command: string, pressed: boolean) {
|
handleCommand (command: string, pressed: boolean) {
|
||||||
|
// Don't allow movement while spectating an entity
|
||||||
|
if (isSpectatingEntity()) return
|
||||||
|
|
||||||
const directionMap = {
|
const directionMap = {
|
||||||
'general.rotateCameraLeft': 'left',
|
'general.rotateCameraLeft': 'left',
|
||||||
'general.rotateCameraRight': 'right',
|
'general.rotateCameraRight': 'right',
|
||||||
|
|
@ -361,6 +377,7 @@ window.cameraRotationControls = cameraRotationControls
|
||||||
const setSneaking = (state: boolean) => {
|
const setSneaking = (state: boolean) => {
|
||||||
gameAdditionalState.isSneaking = state
|
gameAdditionalState.isSneaking = state
|
||||||
bot.setControlState('sneak', state)
|
bot.setControlState('sneak', state)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onTriggerOrReleased = (command: Command, pressed: boolean) => {
|
const onTriggerOrReleased = (command: Command, pressed: boolean) => {
|
||||||
|
|
@ -371,6 +388,7 @@ const onTriggerOrReleased = (command: Command, pressed: boolean) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
|
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case 'general.jump':
|
case 'general.jump':
|
||||||
|
if (isSpectatingEntity()) break
|
||||||
// if (viewer.world.freeFlyMode) {
|
// if (viewer.world.freeFlyMode) {
|
||||||
// const moveSpeed = 0.5
|
// const moveSpeed = 0.5
|
||||||
// viewer.world.freeFlyState.position.add(new Vec3(0, pressed ? moveSpeed : 0, 0))
|
// viewer.world.freeFlyState.position.add(new Vec3(0, pressed ? moveSpeed : 0, 0))
|
||||||
|
|
@ -427,6 +445,28 @@ const onTriggerOrReleased = (command: Command, pressed: boolean) => {
|
||||||
case 'general.playersList':
|
case 'general.playersList':
|
||||||
tabListState.isOpen = pressed
|
tabListState.isOpen = pressed
|
||||||
break
|
break
|
||||||
|
case 'general.viewerConsole':
|
||||||
|
if (lastConnectOptions.value?.viewerWsConnect) {
|
||||||
|
showModal({ reactType: 'console' })
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'general.togglePerspective':
|
||||||
|
if (pressed) {
|
||||||
|
const currentPerspective = playerState.reactive.perspective
|
||||||
|
// eslint-disable-next-line sonarjs/no-nested-switch
|
||||||
|
switch (currentPerspective) {
|
||||||
|
case 'first_person':
|
||||||
|
playerState.reactive.perspective = 'third_person_back'
|
||||||
|
break
|
||||||
|
case 'third_person_back':
|
||||||
|
playerState.reactive.perspective = 'third_person_front'
|
||||||
|
break
|
||||||
|
case 'third_person_front':
|
||||||
|
playerState.reactive.perspective = 'first_person'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
} else if (stringStartsWith(command, 'ui')) {
|
} else if (stringStartsWith(command, 'ui')) {
|
||||||
switch (command) {
|
switch (command) {
|
||||||
|
|
@ -508,6 +548,8 @@ const customCommandsHandler = ({ command }) => {
|
||||||
contro.on('trigger', customCommandsHandler)
|
contro.on('trigger', customCommandsHandler)
|
||||||
|
|
||||||
contro.on('trigger', ({ command }) => {
|
contro.on('trigger', ({ command }) => {
|
||||||
|
if (isCommandDisabled(command)) return
|
||||||
|
|
||||||
const willContinue = !isGameActive(true)
|
const willContinue = !isGameActive(true)
|
||||||
alwaysPressedHandledCommand(command)
|
alwaysPressedHandledCommand(command)
|
||||||
if (willContinue) return
|
if (willContinue) return
|
||||||
|
|
@ -542,13 +584,19 @@ contro.on('trigger', ({ command }) => {
|
||||||
case 'general.debugOverlay':
|
case 'general.debugOverlay':
|
||||||
case 'general.debugOverlayHelpMenu':
|
case 'general.debugOverlayHelpMenu':
|
||||||
case 'general.playersList':
|
case 'general.playersList':
|
||||||
|
case 'general.togglePerspective':
|
||||||
// no-op
|
// no-op
|
||||||
break
|
break
|
||||||
case 'general.swapHands': {
|
case 'general.swapHands': {
|
||||||
bot._client.write('entity_action', {
|
if (isSpectatingEntity()) break
|
||||||
entityId: bot.entity.id,
|
bot._client.write('block_dig', {
|
||||||
actionId: 6,
|
'status': 6,
|
||||||
jumpBoost: 0
|
'location': {
|
||||||
|
'x': 0,
|
||||||
|
'z': 0,
|
||||||
|
'y': 0
|
||||||
|
},
|
||||||
|
'face': 0,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
@ -556,11 +604,13 @@ contro.on('trigger', ({ command }) => {
|
||||||
// handled in onTriggerOrReleased
|
// handled in onTriggerOrReleased
|
||||||
break
|
break
|
||||||
case 'general.inventory':
|
case 'general.inventory':
|
||||||
|
if (isSpectatingEntity()) break
|
||||||
document.exitPointerLock?.()
|
document.exitPointerLock?.()
|
||||||
openPlayerInventory()
|
openPlayerInventory()
|
||||||
break
|
break
|
||||||
case 'general.drop': {
|
case 'general.drop': {
|
||||||
// if (bot.heldItem/* && ctrl */) bot.tossStack(bot.heldItem)
|
if (isSpectatingEntity()) break
|
||||||
|
// protocol 1.9+
|
||||||
bot._client.write('block_dig', {
|
bot._client.write('block_dig', {
|
||||||
'status': 4,
|
'status': 4,
|
||||||
'location': {
|
'location': {
|
||||||
|
|
@ -593,12 +643,15 @@ contro.on('trigger', ({ command }) => {
|
||||||
showModal({ reactType: 'chat' })
|
showModal({ reactType: 'chat' })
|
||||||
break
|
break
|
||||||
case 'general.selectItem':
|
case 'general.selectItem':
|
||||||
|
if (isSpectatingEntity()) break
|
||||||
void selectItem()
|
void selectItem()
|
||||||
break
|
break
|
||||||
case 'general.nextHotbarSlot':
|
case 'general.nextHotbarSlot':
|
||||||
|
if (isSpectatingEntity()) break
|
||||||
cycleHotbarSlot(1)
|
cycleHotbarSlot(1)
|
||||||
break
|
break
|
||||||
case 'general.prevHotbarSlot':
|
case 'general.prevHotbarSlot':
|
||||||
|
if (isSpectatingEntity()) break
|
||||||
cycleHotbarSlot(-1)
|
cycleHotbarSlot(-1)
|
||||||
break
|
break
|
||||||
case 'general.zoom':
|
case 'general.zoom':
|
||||||
|
|
@ -630,6 +683,8 @@ contro.on('trigger', ({ command }) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
contro.on('release', ({ command }) => {
|
contro.on('release', ({ command }) => {
|
||||||
|
if (isCommandDisabled(command)) return
|
||||||
|
|
||||||
inModalCommand(command, false)
|
inModalCommand(command, false)
|
||||||
onTriggerOrReleased(command, false)
|
onTriggerOrReleased(command, false)
|
||||||
})
|
})
|
||||||
|
|
@ -661,6 +716,9 @@ export const f3Keybinds: Array<{
|
||||||
localServer.players[0].world.columns = {}
|
localServer.players[0].world.columns = {}
|
||||||
}
|
}
|
||||||
void reloadChunks()
|
void reloadChunks()
|
||||||
|
if (appViewer.backend?.backendMethods && typeof appViewer.backend.backendMethods.reloadWorld === 'function') {
|
||||||
|
appViewer.backend.backendMethods.reloadWorld()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
mobileTitle: 'Reload chunks',
|
mobileTitle: 'Reload chunks',
|
||||||
},
|
},
|
||||||
|
|
@ -760,6 +818,11 @@ export const f3Keybinds: Array<{
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const reloadChunksAction = () => {
|
||||||
|
const action = f3Keybinds.find(f3Keybind => f3Keybind.key === 'KeyA')
|
||||||
|
void action!.action()
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (!isGameActive(false)) return
|
if (!isGameActive(false)) return
|
||||||
if (contro.pressedKeys.has('F3')) {
|
if (contro.pressedKeys.has('F3')) {
|
||||||
|
|
@ -929,14 +992,17 @@ export function updateBinds (commands: any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const onF3LongPress = async () => {
|
export const onF3LongPress = async () => {
|
||||||
const select = await showOptionsModal('', f3Keybinds.filter(f3Keybind => {
|
const actions = f3Keybinds.filter(f3Keybind => {
|
||||||
return f3Keybind.mobileTitle && (f3Keybind.enabled?.() ?? true)
|
return f3Keybind.mobileTitle && (f3Keybind.enabled?.() ?? true)
|
||||||
}).map(f3Keybind => {
|
})
|
||||||
|
const actionNames = actions.map(f3Keybind => {
|
||||||
return `${f3Keybind.mobileTitle}${f3Keybind.key ? ` (F3+${f3Keybind.key})` : ''}`
|
return `${f3Keybind.mobileTitle}${f3Keybind.key ? ` (F3+${f3Keybind.key})` : ''}`
|
||||||
}))
|
})
|
||||||
|
const select = await showOptionsModal('', actionNames)
|
||||||
if (!select) return
|
if (!select) return
|
||||||
const f3Keybind = f3Keybinds.find(f3Keybind => f3Keybind.mobileTitle === select)
|
const actionIndex = actionNames.indexOf(select)
|
||||||
if (f3Keybind) void f3Keybind.action()
|
const f3Keybind = actions[actionIndex]!
|
||||||
|
void f3Keybind.action()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const handleMobileButtonCustomAction = (action: CustomAction) => {
|
export const handleMobileButtonCustomAction = (action: CustomAction) => {
|
||||||
|
|
@ -946,9 +1012,16 @@ export const handleMobileButtonCustomAction = (action: CustomAction) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const triggerCommand = (command: Command, isDown: boolean) => {
|
||||||
|
handleMobileButtonActionCommand(command, isDown)
|
||||||
|
}
|
||||||
|
|
||||||
export const handleMobileButtonActionCommand = (command: ActionType | ActionHoldConfig, isDown: boolean) => {
|
export const handleMobileButtonActionCommand = (command: ActionType | ActionHoldConfig, isDown: boolean) => {
|
||||||
const commandValue = typeof command === 'string' ? command : 'command' in command ? command.command : command
|
const commandValue = typeof command === 'string' ? command : 'command' in command ? command.command : command
|
||||||
|
|
||||||
|
// Check if command is disabled before proceeding
|
||||||
|
if (typeof commandValue === 'string' && isCommandDisabled(commandValue as Command)) return
|
||||||
|
|
||||||
if (typeof commandValue === 'string' && !stringStartsWith(commandValue, 'custom')) {
|
if (typeof commandValue === 'string' && !stringStartsWith(commandValue, 'custom')) {
|
||||||
const event: CommandEventArgument<typeof contro['_commandsRaw']> = {
|
const event: CommandEventArgument<typeof contro['_commandsRaw']> = {
|
||||||
command: commandValue as Command,
|
command: commandValue as Command,
|
||||||
|
|
|
||||||
106
src/core/ideChannels.ts
Normal file
106
src/core/ideChannels.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { proxy } from 'valtio'
|
||||||
|
|
||||||
|
export const ideState = proxy({
|
||||||
|
id: '',
|
||||||
|
contents: '',
|
||||||
|
line: 0,
|
||||||
|
column: 0,
|
||||||
|
language: 'typescript',
|
||||||
|
title: '',
|
||||||
|
})
|
||||||
|
globalThis.ideState = ideState
|
||||||
|
|
||||||
|
export const registerIdeChannels = () => {
|
||||||
|
registerIdeOpenChannel()
|
||||||
|
registerIdeSaveChannel()
|
||||||
|
}
|
||||||
|
|
||||||
|
const registerIdeOpenChannel = () => {
|
||||||
|
const CHANNEL_NAME = 'minecraft-web-client:ide-open'
|
||||||
|
|
||||||
|
const packetStructure = [
|
||||||
|
'container',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
type: ['pstring', { countType: 'i16' }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'language',
|
||||||
|
type: ['pstring', { countType: 'i16' }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'contents',
|
||||||
|
type: ['pstring', { countType: 'i16' }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'line',
|
||||||
|
type: 'i32'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'column',
|
||||||
|
type: 'i32'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: ['pstring', { countType: 'i16' }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
bot._client.registerChannel(CHANNEL_NAME, packetStructure, true)
|
||||||
|
|
||||||
|
bot._client.on(CHANNEL_NAME as any, (data) => {
|
||||||
|
const { id, language, contents, line, column, title } = data
|
||||||
|
|
||||||
|
ideState.contents = contents
|
||||||
|
ideState.line = line
|
||||||
|
ideState.column = column
|
||||||
|
ideState.id = id
|
||||||
|
ideState.language = language || 'typescript'
|
||||||
|
ideState.title = title
|
||||||
|
})
|
||||||
|
|
||||||
|
console.debug(`registered custom channel ${CHANNEL_NAME} channel`)
|
||||||
|
}
|
||||||
|
const IDE_SAVE_CHANNEL_NAME = 'minecraft-web-client:ide-save'
|
||||||
|
const registerIdeSaveChannel = () => {
|
||||||
|
|
||||||
|
const packetStructure = [
|
||||||
|
'container',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
type: ['pstring', { countType: 'i16' }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'contents',
|
||||||
|
type: ['pstring', { countType: 'i16' }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'language',
|
||||||
|
type: ['pstring', { countType: 'i16' }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'line',
|
||||||
|
type: 'i32'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'column',
|
||||||
|
type: 'i32'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
]
|
||||||
|
bot._client.registerChannel(IDE_SAVE_CHANNEL_NAME, packetStructure, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const saveIde = () => {
|
||||||
|
bot._client.writeChannel(IDE_SAVE_CHANNEL_NAME, {
|
||||||
|
id: ideState.id,
|
||||||
|
contents: ideState.contents,
|
||||||
|
language: ideState.language,
|
||||||
|
// todo: reflect updated
|
||||||
|
line: ideState.line,
|
||||||
|
column: ideState.column,
|
||||||
|
})
|
||||||
|
}
|
||||||
219
src/core/importExport.ts
Normal file
219
src/core/importExport.ts
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
import { appStorage } from '../react/appStorageProvider'
|
||||||
|
import { getChangedSettings, options } from '../optionsStorage'
|
||||||
|
import { customKeymaps } from '../controls'
|
||||||
|
import { showInputsModal } from '../react/SelectOption'
|
||||||
|
|
||||||
|
interface ExportedFile {
|
||||||
|
_about: string
|
||||||
|
options?: Record<string, any>
|
||||||
|
keybindings?: Record<string, any>
|
||||||
|
servers?: any[]
|
||||||
|
username?: string
|
||||||
|
proxy?: string
|
||||||
|
proxies?: string[]
|
||||||
|
accountTokens?: any[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const importData = async () => {
|
||||||
|
try {
|
||||||
|
const input = document.createElement('input')
|
||||||
|
input.type = 'file'
|
||||||
|
input.accept = '.json'
|
||||||
|
input.click()
|
||||||
|
|
||||||
|
const file = await new Promise<File>((resolve) => {
|
||||||
|
input.onchange = () => {
|
||||||
|
if (!input.files?.[0]) return
|
||||||
|
resolve(input.files[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const text = await file.text()
|
||||||
|
const data = JSON.parse(text)
|
||||||
|
|
||||||
|
if (!data._about?.includes('Minecraft Web Client')) {
|
||||||
|
const doContinue = confirm('This file does not appear to be a Minecraft Web Client profile. Continue anyway?')
|
||||||
|
if (!doContinue) return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build available data types for selection
|
||||||
|
const availableData: Record<keyof Omit<ExportedFile, '_about'>, { present: boolean, description: string }> = {
|
||||||
|
options: { present: !!data.options, description: 'Game settings and preferences' },
|
||||||
|
keybindings: { present: !!data.keybindings, description: 'Custom key mappings' },
|
||||||
|
servers: { present: !!data.servers, description: 'Saved server list' },
|
||||||
|
username: { present: !!data.username, description: 'Username' },
|
||||||
|
proxy: { present: !!data.proxy, description: 'Selected proxy server' },
|
||||||
|
proxies: { present: !!data.proxies, description: 'Global proxies list' },
|
||||||
|
accountTokens: { present: !!data.accountTokens, description: 'Account authentication tokens' },
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter to only present data types
|
||||||
|
const presentTypes = Object.fromEntries(Object.entries(availableData)
|
||||||
|
.filter(([_, info]) => info.present)
|
||||||
|
.map<any>(([key, info]) => [key, info]))
|
||||||
|
|
||||||
|
if (Object.keys(presentTypes).length === 0) {
|
||||||
|
alert('No compatible data found in the imported file.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const importChoices = await showInputsModal('Select Data to Import', {
|
||||||
|
mergeData: {
|
||||||
|
type: 'checkbox',
|
||||||
|
label: 'Merge with existing data (uncheck to remove old data)',
|
||||||
|
defaultValue: true,
|
||||||
|
},
|
||||||
|
...Object.fromEntries(Object.entries(presentTypes).map(([key, info]) => [key, {
|
||||||
|
type: 'checkbox',
|
||||||
|
label: info.description,
|
||||||
|
defaultValue: true,
|
||||||
|
}]))
|
||||||
|
}) as { mergeData: boolean } & Record<keyof ExportedFile, boolean>
|
||||||
|
|
||||||
|
if (!importChoices) return
|
||||||
|
|
||||||
|
const importedTypes: string[] = []
|
||||||
|
const shouldMerge = importChoices.mergeData
|
||||||
|
|
||||||
|
if (importChoices.options && data.options) {
|
||||||
|
if (shouldMerge) {
|
||||||
|
Object.assign(options, data.options)
|
||||||
|
} else {
|
||||||
|
for (const key of Object.keys(options)) {
|
||||||
|
if (key in data.options) {
|
||||||
|
options[key as any] = data.options[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
importedTypes.push('settings')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (importChoices.keybindings && data.keybindings) {
|
||||||
|
if (shouldMerge) {
|
||||||
|
Object.assign(customKeymaps, data.keybindings)
|
||||||
|
} else {
|
||||||
|
for (const key of Object.keys(customKeymaps)) delete customKeymaps[key]
|
||||||
|
Object.assign(customKeymaps, data.keybindings)
|
||||||
|
}
|
||||||
|
importedTypes.push('keybindings')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (importChoices.servers && data.servers) {
|
||||||
|
if (shouldMerge && appStorage.serversList) {
|
||||||
|
// Merge by IP, update existing entries and add new ones
|
||||||
|
const existingIps = new Set(appStorage.serversList.map(s => s.ip))
|
||||||
|
const newServers = data.servers.filter(s => !existingIps.has(s.ip))
|
||||||
|
appStorage.serversList = [...appStorage.serversList, ...newServers]
|
||||||
|
} else {
|
||||||
|
appStorage.serversList = data.servers
|
||||||
|
}
|
||||||
|
importedTypes.push('servers')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (importChoices.username && data.username) {
|
||||||
|
appStorage.username = data.username
|
||||||
|
importedTypes.push('username')
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((importChoices.proxy && data.proxy) || (importChoices.proxies && data.proxies)) {
|
||||||
|
if (!appStorage.proxiesData) {
|
||||||
|
appStorage.proxiesData = { proxies: [], selected: '' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (importChoices.proxies && data.proxies) {
|
||||||
|
if (shouldMerge) {
|
||||||
|
// Merge unique proxies
|
||||||
|
const uniqueProxies = new Set([...appStorage.proxiesData.proxies, ...data.proxies])
|
||||||
|
appStorage.proxiesData.proxies = [...uniqueProxies]
|
||||||
|
} else {
|
||||||
|
appStorage.proxiesData.proxies = data.proxies
|
||||||
|
}
|
||||||
|
importedTypes.push('proxies list')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (importChoices.proxy && data.proxy) {
|
||||||
|
appStorage.proxiesData.selected = data.proxy
|
||||||
|
importedTypes.push('selected proxy')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (importChoices.accountTokens && data.accountTokens) {
|
||||||
|
if (shouldMerge && appStorage.authenticatedAccounts) {
|
||||||
|
// Merge by unique identifier (assuming accounts have some unique ID or username)
|
||||||
|
const existingAccounts = new Set(appStorage.authenticatedAccounts.map(a => a.username))
|
||||||
|
const newAccounts = data.accountTokens.filter(a => !existingAccounts.has(a.username))
|
||||||
|
appStorage.authenticatedAccounts = [...appStorage.authenticatedAccounts, ...newAccounts]
|
||||||
|
} else {
|
||||||
|
appStorage.authenticatedAccounts = data.accountTokens
|
||||||
|
}
|
||||||
|
importedTypes.push('account tokens')
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(`Profile imported successfully! Imported data: ${importedTypes.join(', ')}.\nYou may need to reload the page for some changes to take effect.`)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to import profile:', err)
|
||||||
|
alert('Failed to import profile: ' + (err.message || err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const exportData = async () => {
|
||||||
|
const data = await showInputsModal('Export Profile', {
|
||||||
|
profileName: {
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
exportSettings: {
|
||||||
|
type: 'checkbox',
|
||||||
|
defaultValue: true,
|
||||||
|
},
|
||||||
|
exportKeybindings: {
|
||||||
|
type: 'checkbox',
|
||||||
|
defaultValue: true,
|
||||||
|
},
|
||||||
|
exportServers: {
|
||||||
|
type: 'checkbox',
|
||||||
|
defaultValue: true,
|
||||||
|
},
|
||||||
|
saveUsernameAndProxy: {
|
||||||
|
type: 'checkbox',
|
||||||
|
defaultValue: true,
|
||||||
|
},
|
||||||
|
exportGlobalProxiesList: {
|
||||||
|
type: 'checkbox',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
exportAccountTokens: {
|
||||||
|
type: 'checkbox',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const fileName = `${data.profileName ? `${data.profileName}-` : ''}web-client-profile.json`
|
||||||
|
const json: ExportedFile = {
|
||||||
|
_about: 'Minecraft Web Client (mcraft.fun) Profile',
|
||||||
|
...data.exportSettings ? {
|
||||||
|
options: getChangedSettings(),
|
||||||
|
} : {},
|
||||||
|
...data.exportKeybindings ? {
|
||||||
|
keybindings: customKeymaps,
|
||||||
|
} : {},
|
||||||
|
...data.exportServers ? {
|
||||||
|
servers: appStorage.serversList,
|
||||||
|
} : {},
|
||||||
|
...data.saveUsernameAndProxy ? {
|
||||||
|
username: appStorage.username,
|
||||||
|
proxy: appStorage.proxiesData?.selected,
|
||||||
|
} : {},
|
||||||
|
...data.exportGlobalProxiesList ? {
|
||||||
|
proxies: appStorage.proxiesData?.proxies,
|
||||||
|
} : {},
|
||||||
|
...data.exportAccountTokens ? {
|
||||||
|
accountTokens: appStorage.authenticatedAccounts,
|
||||||
|
} : {},
|
||||||
|
}
|
||||||
|
const blob = new Blob([JSON.stringify(json, null, 2)], { type: 'application/json' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = fileName
|
||||||
|
a.click()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
|
@ -2,19 +2,20 @@ import PItem from 'prismarine-item'
|
||||||
import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods'
|
import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods'
|
||||||
import { options } from './optionsStorage'
|
import { options } from './optionsStorage'
|
||||||
import { jeiCustomCategories } from './inventoryWindows'
|
import { jeiCustomCategories } from './inventoryWindows'
|
||||||
|
import { registerIdeChannels } from './core/ideChannels'
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
customEvents.on('mineflayerBotCreated', async () => {
|
customEvents.on('mineflayerBotCreated', async () => {
|
||||||
if (!options.customChannels) return
|
if (!options.customChannels) return
|
||||||
await new Promise(resolve => {
|
bot.once('login', () => {
|
||||||
bot.once('login', () => {
|
registerBlockModelsChannel()
|
||||||
resolve(true)
|
registerMediaChannels()
|
||||||
})
|
registerSectionAnimationChannels()
|
||||||
|
registeredJeiChannel()
|
||||||
|
registerBlockInteractionsCustomizationChannel()
|
||||||
|
registerWaypointChannels()
|
||||||
|
registerIdeChannels()
|
||||||
})
|
})
|
||||||
registerBlockModelsChannel()
|
|
||||||
registerMediaChannels()
|
|
||||||
registerSectionAnimationChannels()
|
|
||||||
registeredJeiChannel()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,6 +33,95 @@ const registerChannel = (channelName: string, packetStructure: any[], handler: (
|
||||||
console.debug(`registered custom channel ${channelName} channel`)
|
console.debug(`registered custom channel ${channelName} channel`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const registerBlockInteractionsCustomizationChannel = () => {
|
||||||
|
const CHANNEL_NAME = 'minecraft-web-client:block-interactions-customization'
|
||||||
|
const packetStructure = [
|
||||||
|
'container',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'newConfiguration',
|
||||||
|
type: ['pstring', { countType: 'i16' }]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
registerChannel(CHANNEL_NAME, packetStructure, (data) => {
|
||||||
|
const config = JSON.parse(data.newConfiguration)
|
||||||
|
bot.mouse.setConfigFromPacket(config)
|
||||||
|
}, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const registerWaypointChannels = () => {
|
||||||
|
const packetStructure = [
|
||||||
|
'container',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
type: ['pstring', { countType: 'i16' }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'x',
|
||||||
|
type: 'f32'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'y',
|
||||||
|
type: 'f32'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'z',
|
||||||
|
type: 'f32'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'minDistance',
|
||||||
|
type: 'i32'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'label',
|
||||||
|
type: ['pstring', { countType: 'i16' }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'color',
|
||||||
|
type: 'i32'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'metadataJson',
|
||||||
|
type: ['pstring', { countType: 'i16' }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
registerChannel('minecraft-web-client:waypoint-add', packetStructure, (data) => {
|
||||||
|
// Parse metadata if provided
|
||||||
|
let metadata: any = {}
|
||||||
|
if (data.metadataJson && data.metadataJson.trim() !== '') {
|
||||||
|
try {
|
||||||
|
metadata = JSON.parse(data.metadataJson)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to parse waypoint metadataJson:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getThreeJsRendererMethods()?.addWaypoint(data.id, data.x, data.y, data.z, {
|
||||||
|
minDistance: data.minDistance,
|
||||||
|
label: data.label || undefined,
|
||||||
|
color: data.color || undefined,
|
||||||
|
metadata
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
registerChannel('minecraft-web-client:waypoint-delete', [
|
||||||
|
'container',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
type: ['pstring', { countType: 'i16' }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
], (data) => {
|
||||||
|
getThreeJsRendererMethods()?.removeWaypoint(data.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const registerBlockModelsChannel = () => {
|
const registerBlockModelsChannel = () => {
|
||||||
const CHANNEL_NAME = 'minecraft-web-client:blockmodels'
|
const CHANNEL_NAME = 'minecraft-web-client:blockmodels'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
|
//@ts-check
|
||||||
|
import * as nbt from 'prismarine-nbt'
|
||||||
import { options } from './optionsStorage'
|
import { options } from './optionsStorage'
|
||||||
|
|
||||||
//@ts-check
|
|
||||||
const { EventEmitter } = require('events')
|
const { EventEmitter } = require('events')
|
||||||
const debug = require('debug')('minecraft-protocol')
|
const debug = require('debug')('minecraft-protocol')
|
||||||
const states = require('minecraft-protocol/src/states')
|
const states = require('minecraft-protocol/src/states')
|
||||||
|
|
@ -51,8 +52,20 @@ class CustomChannelClient extends EventEmitter {
|
||||||
this.emit('state', newProperty, oldProperty)
|
this.emit('state', newProperty, oldProperty)
|
||||||
}
|
}
|
||||||
|
|
||||||
end(reason) {
|
end(endReason, fullReason) {
|
||||||
this._endReason = reason
|
// eslint-disable-next-line unicorn/no-this-assignment
|
||||||
|
const client = this
|
||||||
|
if (client.state === states.PLAY) {
|
||||||
|
fullReason ||= loadedData.supportFeature('chatPacketsUseNbtComponents')
|
||||||
|
? nbt.comp({ text: nbt.string(endReason) })
|
||||||
|
: JSON.stringify({ text: endReason })
|
||||||
|
client.write('kick_disconnect', { reason: fullReason })
|
||||||
|
} else if (client.state === states.LOGIN) {
|
||||||
|
fullReason ||= JSON.stringify({ text: endReason })
|
||||||
|
client.write('disconnect', { reason: fullReason })
|
||||||
|
}
|
||||||
|
|
||||||
|
this._endReason = endReason
|
||||||
this.emit('end', this._endReason) // still emits on server side only, doesn't send anything to our client
|
this.emit('end', this._endReason) // still emits on server side only, doesn't send anything to our client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
import { options } from './optionsStorage'
|
|
||||||
import { assertDefined } from './utils'
|
|
||||||
import { updateBackground } from './water'
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const timeUpdated = () => {
|
|
||||||
// 0 morning
|
|
||||||
const dayTotal = 24_000
|
|
||||||
const evening = 11_500
|
|
||||||
const night = 13_500
|
|
||||||
const morningStart = 23_000
|
|
||||||
const morningEnd = 23_961
|
|
||||||
const timeProgress = options.dayCycleAndLighting ? bot.time.timeOfDay : 0
|
|
||||||
|
|
||||||
// todo check actual colors
|
|
||||||
const dayColorRainy = { r: 111 / 255, g: 156 / 255, b: 236 / 255 }
|
|
||||||
// todo yes, we should make animations (and rain)
|
|
||||||
// eslint-disable-next-line unicorn/numeric-separators-style
|
|
||||||
const dayColor = bot.isRaining ? dayColorRainy : { r: 0.6784313725490196, g: 0.8470588235294118, b: 0.9019607843137255 } // lightblue
|
|
||||||
// let newColor = dayColor
|
|
||||||
let int = 1
|
|
||||||
if (timeProgress < evening) {
|
|
||||||
// stay dayily
|
|
||||||
} else if (timeProgress < night) {
|
|
||||||
const progressNorm = timeProgress - evening
|
|
||||||
const progressMax = night - evening
|
|
||||||
int = 1 - progressNorm / progressMax
|
|
||||||
} else if (timeProgress < morningStart) {
|
|
||||||
int = 0
|
|
||||||
} else if (timeProgress < morningEnd) {
|
|
||||||
const progressNorm = timeProgress - morningStart
|
|
||||||
const progressMax = night - morningEnd
|
|
||||||
int = progressNorm / progressMax
|
|
||||||
}
|
|
||||||
// todo need to think wisely how to set these values & also move directional light around!
|
|
||||||
const colorInt = Math.max(int, 0.1)
|
|
||||||
updateBackground({ r: dayColor.r * colorInt, g: dayColor.g * colorInt, b: dayColor.b * colorInt })
|
|
||||||
if (!options.newVersionsLighting && bot.supportFeature('blockStateId')) {
|
|
||||||
appViewer.playerState.reactive.ambientLight = Math.max(int, 0.25)
|
|
||||||
appViewer.playerState.reactive.directionalLight = Math.min(int, 0.5)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bot.on('time', timeUpdated)
|
|
||||||
timeUpdated()
|
|
||||||
}
|
|
||||||
159
src/defaultOptions.ts
Normal file
159
src/defaultOptions.ts
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
export const defaultOptions = {
|
||||||
|
renderDistance: 3,
|
||||||
|
keepChunksDistance: 1,
|
||||||
|
multiplayerRenderDistance: 3,
|
||||||
|
closeConfirmation: true,
|
||||||
|
autoFullScreen: false,
|
||||||
|
mouseRawInput: true,
|
||||||
|
autoExitFullscreen: false,
|
||||||
|
localUsername: 'wanderer',
|
||||||
|
mouseSensX: 50,
|
||||||
|
mouseSensY: -1,
|
||||||
|
chatWidth: 320,
|
||||||
|
chatHeight: 180,
|
||||||
|
chatScale: 100,
|
||||||
|
chatOpacity: 100,
|
||||||
|
chatOpacityOpened: 100,
|
||||||
|
messagesLimit: 200,
|
||||||
|
volume: 50,
|
||||||
|
enableMusic: true,
|
||||||
|
musicVolume: 50,
|
||||||
|
// fov: 70,
|
||||||
|
fov: 75,
|
||||||
|
defaultPerspective: 'first_person' as 'first_person' | 'third_person_back' | 'third_person_front',
|
||||||
|
guiScale: 3,
|
||||||
|
autoRequestCompletions: true,
|
||||||
|
touchButtonsSize: 40,
|
||||||
|
touchButtonsOpacity: 80,
|
||||||
|
touchButtonsPosition: 12,
|
||||||
|
touchControlsPositions: getDefaultTouchControlsPositions(),
|
||||||
|
touchControlsSize: getTouchControlsSize(),
|
||||||
|
touchMovementType: 'modern' as 'modern' | 'classic',
|
||||||
|
touchInteractionType: 'classic' as 'classic' | 'buttons',
|
||||||
|
gpuPreference: 'default' as 'default' | 'high-performance' | 'low-power',
|
||||||
|
backgroundRendering: '20fps' as 'full' | '20fps' | '5fps',
|
||||||
|
/** @unstable */
|
||||||
|
disableAssets: false,
|
||||||
|
/** @unstable */
|
||||||
|
debugLogNotFrequentPackets: false,
|
||||||
|
unimplementedContainers: false,
|
||||||
|
dayCycleAndLighting: true,
|
||||||
|
loadPlayerSkins: true,
|
||||||
|
renderEars: true,
|
||||||
|
lowMemoryMode: false,
|
||||||
|
starfieldRendering: true,
|
||||||
|
defaultSkybox: true,
|
||||||
|
enabledResourcepack: null as string | null,
|
||||||
|
useVersionsTextures: 'latest',
|
||||||
|
serverResourcePacks: 'prompt' as 'prompt' | 'always' | 'never',
|
||||||
|
showHand: true,
|
||||||
|
viewBobbing: true,
|
||||||
|
displayRecordButton: true,
|
||||||
|
packetsLoggerPreset: 'all' as 'all' | 'no-buffers',
|
||||||
|
serversAutoVersionSelect: 'auto' as 'auto' | 'latest' | '1.20.4' | string,
|
||||||
|
customChannels: false,
|
||||||
|
remoteContentNotSameOrigin: false as boolean | string[],
|
||||||
|
packetsRecordingAutoStart: false,
|
||||||
|
language: 'auto',
|
||||||
|
preciseMouseInput: false,
|
||||||
|
// todo ui setting, maybe enable by default?
|
||||||
|
waitForChunksRender: false as 'sp-only' | boolean,
|
||||||
|
jeiEnabled: true as boolean | Array<'creative' | 'survival' | 'adventure' | 'spectator'>,
|
||||||
|
modsSupport: false,
|
||||||
|
modsAutoUpdate: 'check' as 'check' | 'never' | 'always',
|
||||||
|
modsUpdatePeriodCheck: 24, // hours
|
||||||
|
preventBackgroundTimeoutKick: false,
|
||||||
|
preventSleep: false,
|
||||||
|
debugContro: false,
|
||||||
|
debugChatScroll: false,
|
||||||
|
chatVanillaRestrictions: true,
|
||||||
|
debugResponseTimeIndicator: false,
|
||||||
|
chatPingExtension: true,
|
||||||
|
// antiAliasing: false,
|
||||||
|
topRightTimeDisplay: 'only-fullscreen' as 'only-fullscreen' | 'always' | 'never',
|
||||||
|
|
||||||
|
clipWorldBelowY: undefined as undefined | number, // will be removed
|
||||||
|
disableSignsMapsSupport: false,
|
||||||
|
singleplayerAutoSave: false,
|
||||||
|
showChunkBorders: false, // todo rename option
|
||||||
|
frameLimit: false as number | false,
|
||||||
|
alwaysBackupWorldBeforeLoading: undefined as boolean | undefined | null,
|
||||||
|
alwaysShowMobileControls: false,
|
||||||
|
excludeCommunicationDebugEvents: [] as string[],
|
||||||
|
preventDevReloadWhilePlaying: false,
|
||||||
|
numWorkers: 4,
|
||||||
|
localServerOptions: {
|
||||||
|
gameMode: 1
|
||||||
|
} as any,
|
||||||
|
saveLoginPassword: 'prompt' as 'prompt' | 'never' | 'always',
|
||||||
|
preferLoadReadonly: false,
|
||||||
|
experimentalClientSelfReload: false,
|
||||||
|
remoteSoundsSupport: false,
|
||||||
|
remoteSoundsLoadTimeout: 500,
|
||||||
|
disableLoadPrompts: false,
|
||||||
|
guestUsername: 'guest',
|
||||||
|
askGuestName: true,
|
||||||
|
errorReporting: true,
|
||||||
|
/** Actually might be useful */
|
||||||
|
showCursorBlockInSpectator: false,
|
||||||
|
renderEntities: true,
|
||||||
|
smoothLighting: true,
|
||||||
|
newVersionsLighting: false,
|
||||||
|
chatSelect: true,
|
||||||
|
autoJump: 'auto' as 'auto' | 'always' | 'never',
|
||||||
|
autoParkour: false,
|
||||||
|
vrSupport: true, // doesn't directly affect the VR mode, should only disable the button which is annoying to android users
|
||||||
|
vrPageGameRendering: false,
|
||||||
|
renderDebug: 'basic' as 'none' | 'advanced' | 'basic',
|
||||||
|
rendererPerfDebugOverlay: false,
|
||||||
|
|
||||||
|
// advanced bot options
|
||||||
|
autoRespawn: false,
|
||||||
|
mutedSounds: [] as string[],
|
||||||
|
plugins: [] as Array<{ enabled: boolean, name: string, description: string, script: string }>,
|
||||||
|
/** Wether to popup sign editor on server action */
|
||||||
|
autoSignEditor: true,
|
||||||
|
wysiwygSignEditor: 'auto' as 'auto' | 'always' | 'never',
|
||||||
|
showMinimap: 'never' as 'always' | 'singleplayer' | 'never',
|
||||||
|
minimapOptimizations: true,
|
||||||
|
displayBossBars: true,
|
||||||
|
disabledUiParts: [] as string[],
|
||||||
|
neighborChunkUpdates: true,
|
||||||
|
highlightBlockColor: 'auto' as 'auto' | 'blue' | 'classic',
|
||||||
|
activeRenderer: 'threejs',
|
||||||
|
rendererSharedOptions: {
|
||||||
|
_experimentalSmoothChunkLoading: true,
|
||||||
|
_renderByChunks: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultTouchControlsPositions () {
|
||||||
|
return {
|
||||||
|
action: [
|
||||||
|
70,
|
||||||
|
76
|
||||||
|
],
|
||||||
|
sneak: [
|
||||||
|
84,
|
||||||
|
76
|
||||||
|
],
|
||||||
|
break: [
|
||||||
|
70,
|
||||||
|
57
|
||||||
|
],
|
||||||
|
jump: [
|
||||||
|
84,
|
||||||
|
57
|
||||||
|
],
|
||||||
|
} as Record<string, [number, number]>
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTouchControlsSize () {
|
||||||
|
return {
|
||||||
|
joystick: 55,
|
||||||
|
action: 36,
|
||||||
|
break: 36,
|
||||||
|
jump: 36,
|
||||||
|
sneak: 36,
|
||||||
|
}
|
||||||
|
}
|
||||||
113
src/devtools.ts
113
src/devtools.ts
|
|
@ -5,6 +5,17 @@ import { WorldRendererThree } from 'renderer/viewer/three/worldrendererThree'
|
||||||
import { enable, disable, enabled } from 'debug'
|
import { enable, disable, enabled } from 'debug'
|
||||||
import { Vec3 } from 'vec3'
|
import { Vec3 } from 'vec3'
|
||||||
|
|
||||||
|
customEvents.on('mineflayerBotCreated', () => {
|
||||||
|
window.debugServerPacketNames = Object.fromEntries(Object.keys(loadedData.protocol.play.toClient.types).map(name => {
|
||||||
|
name = name.replace('packet_', '')
|
||||||
|
return [name, name]
|
||||||
|
}))
|
||||||
|
window.debugClientPacketNames = Object.fromEntries(Object.keys(loadedData.protocol.play.toServer.types).map(name => {
|
||||||
|
name = name.replace('packet_', '')
|
||||||
|
return [name, name]
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
window.Vec3 = Vec3
|
window.Vec3 = Vec3
|
||||||
window.cursorBlockRel = (x = 0, y = 0, z = 0) => {
|
window.cursorBlockRel = (x = 0, y = 0, z = 0) => {
|
||||||
const newPos = bot.blockAtCursor(5)?.position.offset(x, y, z)
|
const newPos = bot.blockAtCursor(5)?.position.offset(x, y, z)
|
||||||
|
|
@ -209,3 +220,105 @@ setInterval(() => {
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
|
|
||||||
|
// Add type declaration for performance.memory
|
||||||
|
declare global {
|
||||||
|
interface Performance {
|
||||||
|
memory?: {
|
||||||
|
usedJSHeapSize: number
|
||||||
|
totalJSHeapSize: number
|
||||||
|
jsHeapSizeLimit: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performance metrics WebSocket client
|
||||||
|
let ws: WebSocket | null = null
|
||||||
|
let wsReconnectTimeout: NodeJS.Timeout | null = null
|
||||||
|
let metricsInterval: NodeJS.Timeout | null = null
|
||||||
|
|
||||||
|
// Start collecting metrics immediately
|
||||||
|
const startTime = performance.now()
|
||||||
|
|
||||||
|
function collectAndSendMetrics () {
|
||||||
|
if (!ws || ws.readyState !== WebSocket.OPEN) return
|
||||||
|
|
||||||
|
const metrics = {
|
||||||
|
loadTime: performance.now() - startTime,
|
||||||
|
memoryUsage: (performance.memory?.usedJSHeapSize ?? 0) / 1024 / 1024,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.send(JSON.stringify(metrics))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWebSocketUrl () {
|
||||||
|
const wsPort = process.env.WS_PORT
|
||||||
|
if (!wsPort) return null
|
||||||
|
|
||||||
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||||
|
const { hostname } = window.location
|
||||||
|
return `${protocol}//${hostname}:${wsPort}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectWebSocket () {
|
||||||
|
if (ws) return
|
||||||
|
|
||||||
|
const wsUrl = getWebSocketUrl()
|
||||||
|
if (!wsUrl) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ws = new WebSocket(wsUrl)
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('Connected to metrics server')
|
||||||
|
if (wsReconnectTimeout) {
|
||||||
|
clearTimeout(wsReconnectTimeout)
|
||||||
|
wsReconnectTimeout = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start sending metrics immediately after connection
|
||||||
|
collectAndSendMetrics()
|
||||||
|
|
||||||
|
// Clear existing interval if any
|
||||||
|
if (metricsInterval) {
|
||||||
|
clearInterval(metricsInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new interval
|
||||||
|
metricsInterval = setInterval(collectAndSendMetrics, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
console.log('Disconnected from metrics server')
|
||||||
|
ws = null
|
||||||
|
|
||||||
|
// Clear metrics interval
|
||||||
|
if (metricsInterval) {
|
||||||
|
clearInterval(metricsInterval)
|
||||||
|
metricsInterval = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to reconnect after 3 seconds
|
||||||
|
wsReconnectTimeout = setTimeout(connectWebSocket, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
console.error('WebSocket error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect immediately
|
||||||
|
connectWebSocket()
|
||||||
|
|
||||||
|
// Add command to request current metrics
|
||||||
|
window.requestMetrics = () => {
|
||||||
|
const metrics = {
|
||||||
|
loadTime: performance.now() - startTime,
|
||||||
|
memoryUsage: (performance.memory?.usedJSHeapSize ?? 0) / 1024 / 1024,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}
|
||||||
|
console.log('Current metrics:', metrics)
|
||||||
|
return metrics
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,12 @@ export const getFixedFilesize = (bytes: number) => {
|
||||||
return prettyBytes(bytes, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
return prettyBytes(bytes, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isInterestedInDownload = () => {
|
||||||
|
const { map, texturepack, replayFileUrl } = appQueryParams
|
||||||
|
const { mapDir } = appQueryParamsArray
|
||||||
|
return !!map || !!texturepack || !!replayFileUrl || !!mapDir
|
||||||
|
}
|
||||||
|
|
||||||
const inner = async () => {
|
const inner = async () => {
|
||||||
const { map, texturepack, replayFileUrl } = appQueryParams
|
const { map, texturepack, replayFileUrl } = appQueryParams
|
||||||
const { mapDir } = appQueryParamsArray
|
const { mapDir } = appQueryParamsArray
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import fs from 'fs'
|
||||||
import * as nbt from 'prismarine-nbt'
|
import * as nbt from 'prismarine-nbt'
|
||||||
import RegionFile from 'prismarine-provider-anvil/src/region'
|
import RegionFile from 'prismarine-provider-anvil/src/region'
|
||||||
import { versions } from 'minecraft-data'
|
import { versions } from 'minecraft-data'
|
||||||
|
import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods'
|
||||||
import { openWorldDirectory, openWorldZip } from './browserfs'
|
import { openWorldDirectory, openWorldZip } from './browserfs'
|
||||||
import { isGameActive } from './globalState'
|
import { isGameActive } from './globalState'
|
||||||
import { showNotification } from './react/NotificationProvider'
|
import { showNotification } from './react/NotificationProvider'
|
||||||
|
|
@ -12,6 +13,9 @@ const parseNbt = promisify(nbt.parse)
|
||||||
const simplifyNbt = nbt.simplify
|
const simplifyNbt = nbt.simplify
|
||||||
window.nbt = nbt
|
window.nbt = nbt
|
||||||
|
|
||||||
|
// Supported image types for skybox
|
||||||
|
const VALID_IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.webp']
|
||||||
|
|
||||||
// todo display drop zone
|
// todo display drop zone
|
||||||
for (const event of ['drag', 'dragstart', 'dragend', 'dragover', 'dragenter', 'dragleave', 'drop']) {
|
for (const event of ['drag', 'dragstart', 'dragend', 'dragover', 'dragenter', 'dragleave', 'drop']) {
|
||||||
window.addEventListener(event, (e: any) => {
|
window.addEventListener(event, (e: any) => {
|
||||||
|
|
@ -45,6 +49,34 @@ window.addEventListener('drop', async e => {
|
||||||
})
|
})
|
||||||
|
|
||||||
async function handleDroppedFile (file: File) {
|
async function handleDroppedFile (file: File) {
|
||||||
|
// Check for image files first when game is active
|
||||||
|
if (isGameActive(false) && VALID_IMAGE_EXTENSIONS.some(ext => file.name.toLowerCase().endsWith(ext))) {
|
||||||
|
try {
|
||||||
|
// Convert image to base64
|
||||||
|
const reader = new FileReader()
|
||||||
|
const base64Promise = new Promise<string>((resolve, reject) => {
|
||||||
|
reader.onload = () => resolve(reader.result as string)
|
||||||
|
reader.onerror = reject
|
||||||
|
})
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
const base64Image = await base64Promise
|
||||||
|
|
||||||
|
// Get ThreeJS backend methods and update skybox
|
||||||
|
const setSkyboxImage = getThreeJsRendererMethods()?.setSkyboxImage
|
||||||
|
if (setSkyboxImage) {
|
||||||
|
await setSkyboxImage(base64Image)
|
||||||
|
showNotification('Skybox updated successfully')
|
||||||
|
} else {
|
||||||
|
showNotification('Cannot update skybox - renderer does not support it')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to update skybox:', err)
|
||||||
|
showNotification('Failed to update skybox', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (file.name.endsWith('.zip')) {
|
if (file.name.endsWith('.zip')) {
|
||||||
void openWorldZip(file)
|
void openWorldZip(file)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
285
src/entities.ts
285
src/entities.ts
|
|
@ -4,8 +4,9 @@ import tracker from '@nxg-org/mineflayer-tracker'
|
||||||
import { loader as autoJumpPlugin } from '@nxg-org/mineflayer-auto-jump'
|
import { loader as autoJumpPlugin } from '@nxg-org/mineflayer-auto-jump'
|
||||||
import { subscribeKey } from 'valtio/utils'
|
import { subscribeKey } from 'valtio/utils'
|
||||||
import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods'
|
import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods'
|
||||||
|
import { Team } from 'mineflayer'
|
||||||
import { options, watchValue } from './optionsStorage'
|
import { options, watchValue } from './optionsStorage'
|
||||||
import { miscUiState } from './globalState'
|
import { gameAdditionalState, miscUiState } from './globalState'
|
||||||
import { EntityStatus } from './mineflayer/entityStatus'
|
import { EntityStatus } from './mineflayer/entityStatus'
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -43,7 +44,7 @@ customEvents.on('gameLoaded', () => {
|
||||||
updateAutoJump()
|
updateAutoJump()
|
||||||
|
|
||||||
const playerPerAnimation = {} as Record<string, string>
|
const playerPerAnimation = {} as Record<string, string>
|
||||||
const entityData = (e: Entity) => {
|
const checkEntityData = (e: Entity) => {
|
||||||
if (!e.username) return
|
if (!e.username) return
|
||||||
window.debugEntityMetadata ??= {}
|
window.debugEntityMetadata ??= {}
|
||||||
window.debugEntityMetadata[e.username] = e
|
window.debugEntityMetadata[e.username] = e
|
||||||
|
|
@ -52,6 +53,13 @@ customEvents.on('gameLoaded', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const trackBotEntity = () => {
|
||||||
|
// Always track the bot entity for animations
|
||||||
|
if (bot.entity) {
|
||||||
|
bot.tracker.trackEntity(bot.entity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let lastCall = 0
|
let lastCall = 0
|
||||||
bot.on('physicsTick', () => {
|
bot.on('physicsTick', () => {
|
||||||
// throttle, tps: 6
|
// throttle, tps: 6
|
||||||
|
|
@ -64,7 +72,7 @@ customEvents.on('gameLoaded', () => {
|
||||||
const speed = info.avgVel
|
const speed = info.avgVel
|
||||||
const WALKING_SPEED = 0.03
|
const WALKING_SPEED = 0.03
|
||||||
const SPRINTING_SPEED = 0.18
|
const SPRINTING_SPEED = 0.18
|
||||||
const isCrouched = e['crouching']
|
const isCrouched = e === bot.entity ? gameAdditionalState.isSneaking : e['crouching']
|
||||||
const isWalking = Math.abs(speed.x) > WALKING_SPEED || Math.abs(speed.z) > WALKING_SPEED
|
const isWalking = Math.abs(speed.x) > WALKING_SPEED || Math.abs(speed.z) > WALKING_SPEED
|
||||||
const isSprinting = Math.abs(speed.x) > SPRINTING_SPEED || Math.abs(speed.z) > SPRINTING_SPEED
|
const isSprinting = Math.abs(speed.x) > SPRINTING_SPEED || Math.abs(speed.z) > SPRINTING_SPEED
|
||||||
|
|
||||||
|
|
@ -73,7 +81,12 @@ customEvents.on('gameLoaded', () => {
|
||||||
: isWalking ? (isSprinting ? 'running' : 'walking')
|
: isWalking ? (isSprinting ? 'running' : 'walking')
|
||||||
: 'idle'
|
: 'idle'
|
||||||
if (newAnimation !== playerPerAnimation[id]) {
|
if (newAnimation !== playerPerAnimation[id]) {
|
||||||
getThreeJsRendererMethods()?.playEntityAnimation(e.id, newAnimation)
|
// Handle bot entity animation specially (for player entity in third person)
|
||||||
|
if (e === bot.entity) {
|
||||||
|
getThreeJsRendererMethods()?.playEntityAnimation('player_entity', newAnimation)
|
||||||
|
} else {
|
||||||
|
getThreeJsRendererMethods()?.playEntityAnimation(e.id, newAnimation)
|
||||||
|
}
|
||||||
playerPerAnimation[id] = newAnimation
|
playerPerAnimation[id] = newAnimation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -83,6 +96,25 @@ customEvents.on('gameLoaded', () => {
|
||||||
getThreeJsRendererMethods()?.playEntityAnimation(e.id, 'oneSwing')
|
getThreeJsRendererMethods()?.playEntityAnimation(e.id, 'oneSwing')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
bot.on('botArmSwingStart', (hand) => {
|
||||||
|
if (hand === 'right') {
|
||||||
|
getThreeJsRendererMethods()?.playEntityAnimation('player_entity', 'oneSwing')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
bot.inventory.on('updateSlot', (slot) => {
|
||||||
|
if (slot === 5 || slot === 6 || slot === 7 || slot === 8) {
|
||||||
|
const item = bot.inventory.slots[slot]!
|
||||||
|
bot.entity.equipment[slot - 3] = item
|
||||||
|
appViewer.worldView?.emit('playerEntity', bot.entity)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
bot.on('heldItemChanged', () => {
|
||||||
|
const item = bot.inventory.slots[bot.quickBarSlot + 36]!
|
||||||
|
bot.entity.equipment[0] = item
|
||||||
|
appViewer.worldView?.emit('playerEntity', bot.entity)
|
||||||
|
})
|
||||||
|
|
||||||
bot._client.on('damage_event', (data) => {
|
bot._client.on('damage_event', (data) => {
|
||||||
const { entityId, sourceTypeId: damage } = data
|
const { entityId, sourceTypeId: damage } = data
|
||||||
getThreeJsRendererMethods()?.damageEntity(entityId, damage)
|
getThreeJsRendererMethods()?.damageEntity(entityId, damage)
|
||||||
|
|
@ -94,60 +126,243 @@ customEvents.on('gameLoaded', () => {
|
||||||
if (entityStatus === EntityStatus.HURT) {
|
if (entityStatus === EntityStatus.HURT) {
|
||||||
getThreeJsRendererMethods()?.damageEntity(entityId, entityStatus)
|
getThreeJsRendererMethods()?.damageEntity(entityId, entityStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (entityStatus === EntityStatus.BURNED) {
|
||||||
|
updateEntityStates(entityId, true, true)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// on fire events
|
||||||
|
bot._client.on('entity_metadata', (data) => {
|
||||||
|
if (data.entityId !== bot.entity.id) return
|
||||||
|
handleEntityMetadata(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
bot.on('end', () => {
|
||||||
|
if (onFireTimeout) {
|
||||||
|
clearTimeout(onFireTimeout)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
bot.on('respawn', () => {
|
||||||
|
if (onFireTimeout) {
|
||||||
|
clearTimeout(onFireTimeout)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateCamera = (entity: Entity) => {
|
||||||
|
if (bot.game.gameMode !== 'spectator') return
|
||||||
|
bot.entity.position = entity.position.clone()
|
||||||
|
void bot.look(entity.yaw, entity.pitch, true)
|
||||||
|
bot.entity.yaw = entity.yaw
|
||||||
|
bot.entity.pitch = entity.pitch
|
||||||
|
}
|
||||||
|
|
||||||
bot.on('entityGone', (entity) => {
|
bot.on('entityGone', (entity) => {
|
||||||
bot.tracker.stopTrackingEntity(entity, true)
|
bot.tracker.stopTrackingEntity(entity, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
bot.on('entityMoved', (e) => {
|
bot.on('entityMoved', (e) => {
|
||||||
entityData(e)
|
checkEntityData(e)
|
||||||
|
if (appViewer.playerState.reactive.cameraSpectatingEntity === e.id) {
|
||||||
|
updateCamera(e)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
bot._client.on('entity_velocity', (packet) => {
|
bot._client.on('entity_velocity', (packet) => {
|
||||||
const e = bot.entities[packet.entityId]
|
const e = bot.entities[packet.entityId]
|
||||||
if (!e) return
|
if (!e) return
|
||||||
entityData(e)
|
checkEntityData(e)
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const entity of Object.values(bot.entities)) {
|
for (const entity of Object.values(bot.entities)) {
|
||||||
if (entity !== bot.entity) {
|
if (entity !== bot.entity) {
|
||||||
entityData(entity)
|
checkEntityData(entity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bot.on('entitySpawn', entityData)
|
// Track bot entity initially
|
||||||
bot.on('entityUpdate', entityData)
|
trackBotEntity()
|
||||||
bot.on('entityEquip', entityData)
|
|
||||||
|
bot.on('entitySpawn', (e) => {
|
||||||
|
checkEntityData(e)
|
||||||
|
if (appViewer.playerState.reactive.cameraSpectatingEntity === e.id) {
|
||||||
|
updateCamera(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
bot.on('entityUpdate', checkEntityData)
|
||||||
|
bot.on('entityEquip', checkEntityData)
|
||||||
|
|
||||||
|
// Re-track bot entity after login
|
||||||
|
bot.on('login', () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
trackBotEntity()
|
||||||
|
}) // Small delay to ensure bot.entity is properly set
|
||||||
|
})
|
||||||
|
|
||||||
|
bot._client.on('camera', (packet) => {
|
||||||
|
if (bot.player.entity.id === packet.cameraId) {
|
||||||
|
if (appViewer.playerState.utils.isSpectatingEntity() && appViewer.playerState.reactive.cameraSpectatingEntity) {
|
||||||
|
const entity = bot.entities[appViewer.playerState.reactive.cameraSpectatingEntity]
|
||||||
|
appViewer.playerState.reactive.cameraSpectatingEntity = undefined
|
||||||
|
if (entity) {
|
||||||
|
// do a force entity update
|
||||||
|
bot.emit('entityUpdate', entity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (appViewer.playerState.reactive.gameMode === 'spectator') {
|
||||||
|
const entity = bot.entities[packet.cameraId]
|
||||||
|
appViewer.playerState.reactive.cameraSpectatingEntity = packet.cameraId
|
||||||
|
if (entity) {
|
||||||
|
updateCamera(entity)
|
||||||
|
// do a force entity update
|
||||||
|
bot.emit('entityUpdate', entity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const applySkinTexturesProxy = (url: string | undefined) => {
|
||||||
|
const { appConfig } = miscUiState
|
||||||
|
if (appConfig?.skinTexturesProxy) {
|
||||||
|
return url?.replace('http://textures.minecraft.net/', appConfig.skinTexturesProxy)
|
||||||
|
.replace('https://textures.minecraft.net/', appConfig.skinTexturesProxy)
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
// Texture override from packet properties
|
// Texture override from packet properties
|
||||||
bot._client.on('player_info', (packet) => {
|
const updateSkin = (player: import('mineflayer').Player) => {
|
||||||
for (const playerEntry of packet.data) {
|
if (!player.uuid || !player.username || !player.skinData) return
|
||||||
if (!playerEntry.player && !playerEntry.properties) continue
|
|
||||||
let textureProperty = playerEntry.properties?.find(prop => prop?.name === 'textures')
|
|
||||||
if (!textureProperty) {
|
|
||||||
textureProperty = playerEntry.player?.properties?.find(prop => prop?.key === 'textures')
|
|
||||||
}
|
|
||||||
if (textureProperty) {
|
|
||||||
try {
|
|
||||||
const textureData = JSON.parse(Buffer.from(textureProperty.value, 'base64').toString())
|
|
||||||
const skinUrl = textureData.textures?.SKIN?.url
|
|
||||||
const capeUrl = textureData.textures?.CAPE?.url
|
|
||||||
|
|
||||||
// Find entity with matching UUID and update skin
|
try {
|
||||||
let entityId = ''
|
const skinUrl = applySkinTexturesProxy(player.skinData.url)
|
||||||
for (const [entId, entity] of Object.entries(bot.entities)) {
|
const capeUrl = applySkinTexturesProxy((player.skinData as any).capeUrl)
|
||||||
if (entity.uuid === playerEntry.uuid) {
|
|
||||||
entityId = entId
|
// Find entity with matching UUID and update skin
|
||||||
break
|
let entityId = ''
|
||||||
}
|
for (const [entId, entity] of Object.entries(bot.entities)) {
|
||||||
}
|
if (entity.uuid === player.uuid) {
|
||||||
// even if not found, still record to cache
|
entityId = entId
|
||||||
void getThreeJsRendererMethods()?.updatePlayerSkin(entityId, playerEntry.player?.name, playerEntry.uuid, skinUrl, capeUrl)
|
break
|
||||||
} catch (err) {
|
}
|
||||||
console.error('Error decoding player texture:', err)
|
}
|
||||||
|
// even if not found, still record to cache
|
||||||
|
void getThreeJsRendererMethods()!.updatePlayerSkin(entityId, player.username, player.uuid, skinUrl ?? true, capeUrl)
|
||||||
|
} catch (err) {
|
||||||
|
reportError(new Error('Error applying skin texture:', { cause: err }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bot.on('playerJoined', updateSkin)
|
||||||
|
bot.on('playerUpdated', updateSkin)
|
||||||
|
for (const entity of Object.values(bot.players)) {
|
||||||
|
updateSkin(entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
const teamUpdated = (team: Team) => {
|
||||||
|
for (const entity of Object.values(bot.entities)) {
|
||||||
|
if (entity.type === 'player' && entity.username && team.members.includes(entity.username) || entity.uuid && team.members.includes(entity.uuid)) {
|
||||||
|
bot.emit('entityUpdate', entity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bot.on('teamUpdated', teamUpdated)
|
||||||
|
for (const team of Object.values(bot.teams)) {
|
||||||
|
teamUpdated(team)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateEntityNameTags = (team: Team) => {
|
||||||
|
for (const entity of Object.values(bot.entities)) {
|
||||||
|
const entityTeam = entity.type === 'player' && entity.username ? bot.teamMap[entity.username] : entity.uuid ? bot.teamMap[entity.uuid] : undefined
|
||||||
|
if ((entityTeam?.nameTagVisibility === 'hideForOwnTeam' && entityTeam.name === team.name)
|
||||||
|
|| (entityTeam?.nameTagVisibility === 'hideForOtherTeams' && entityTeam.name !== team.name)) {
|
||||||
|
bot.emit('entityUpdate', entity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const doEntitiesNeedUpdating = (team: Team) => {
|
||||||
|
return team.nameTagVisibility === 'never'
|
||||||
|
|| (team.nameTagVisibility === 'hideForOtherTeams' && appViewer.playerState.reactive.team?.team !== team.team)
|
||||||
|
|| (team.nameTagVisibility === 'hideForOwnTeam' && appViewer.playerState.reactive.team?.team === team.team)
|
||||||
|
}
|
||||||
|
|
||||||
|
bot.on('teamMemberAdded', (team: Team, members: string[]) => {
|
||||||
|
if (members.includes(bot.username) && appViewer.playerState.reactive.team?.team !== team.team) {
|
||||||
|
appViewer.playerState.reactive.team = team
|
||||||
|
// Player was added to a team, need to check if any entities need updating
|
||||||
|
updateEntityNameTags(team)
|
||||||
|
} else if (doEntitiesNeedUpdating(team)) {
|
||||||
|
// Need to update all entities that were added
|
||||||
|
for (const entity of Object.values(bot.entities)) {
|
||||||
|
if (entity.type === 'player' && entity.username && members.includes(entity.username) || entity.uuid && members.includes(entity.uuid)) {
|
||||||
|
bot.emit('entityUpdate', entity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
bot.on('teamMemberRemoved', (team: Team, members: string[]) => {
|
||||||
|
if (members.includes(bot.username) && appViewer.playerState.reactive.team?.team === team.team) {
|
||||||
|
appViewer.playerState.reactive.team = undefined
|
||||||
|
// Player was removed from a team, need to check if any entities need updating
|
||||||
|
updateEntityNameTags(team)
|
||||||
|
} else if (doEntitiesNeedUpdating(team)) {
|
||||||
|
// Need to update all entities that were removed
|
||||||
|
for (const entity of Object.values(bot.entities)) {
|
||||||
|
if (entity.type === 'player' && entity.username && members.includes(entity.username) || entity.uuid && members.includes(entity.uuid)) {
|
||||||
|
bot.emit('entityUpdate', entity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
bot.on('teamRemoved', (team: Team) => {
|
||||||
|
if (appViewer.playerState.reactive.team?.team === team?.team) {
|
||||||
|
appViewer.playerState.reactive.team = undefined
|
||||||
|
// Player's team was removed, need to update all entities that are in a team
|
||||||
|
updateEntityNameTags(team)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const SHARED_FLAGS_KEY = 0
|
||||||
|
const ENTITY_FLAGS = {
|
||||||
|
ON_FIRE: 0x01, // Bit 0
|
||||||
|
SNEAKING: 0x02, // Bit 1
|
||||||
|
SPRINTING: 0x08, // Bit 3
|
||||||
|
SWIMMING: 0x10, // Bit 4
|
||||||
|
INVISIBLE: 0x20, // Bit 5
|
||||||
|
GLOWING: 0x40, // Bit 6
|
||||||
|
FALL_FLYING: 0x80 // Bit 7 (elytra flying)
|
||||||
|
}
|
||||||
|
|
||||||
|
let onFireTimeout: NodeJS.Timeout | undefined
|
||||||
|
const updateEntityStates = (entityId: number, onFire: boolean, timeout?: boolean) => {
|
||||||
|
if (entityId !== bot.entity.id) return
|
||||||
|
appViewer.playerState.reactive.onFire = onFire
|
||||||
|
if (onFireTimeout) {
|
||||||
|
clearTimeout(onFireTimeout)
|
||||||
|
}
|
||||||
|
if (timeout) {
|
||||||
|
onFireTimeout = setTimeout(() => {
|
||||||
|
updateEntityStates(entityId, false, false)
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process entity metadata packet
|
||||||
|
function handleEntityMetadata (packet: { entityId: number, metadata: Array<{ key: number, type: string, value: number }> }) {
|
||||||
|
const { entityId, metadata } = packet
|
||||||
|
|
||||||
|
// Find shared flags in metadata
|
||||||
|
const flagsData = metadata.find(meta => meta.key === SHARED_FLAGS_KEY &&
|
||||||
|
meta.type === 'byte')
|
||||||
|
|
||||||
|
// Update fire state if flags were found
|
||||||
|
if (flagsData) {
|
||||||
|
const wasOnFire = appViewer.playerState.reactive.onFire
|
||||||
|
appViewer.playerState.reactive.onFire = (flagsData.value & ENTITY_FLAGS.ON_FIRE) !== 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
37
src/env.d.ts
vendored
Normal file
37
src/env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
declare namespace NodeJS {
|
||||||
|
interface ProcessEnv {
|
||||||
|
// Build configuration
|
||||||
|
NODE_ENV: 'development' | 'production'
|
||||||
|
MIN_MC_VERSION?: string
|
||||||
|
MAX_MC_VERSION?: string
|
||||||
|
ALWAYS_COMPRESS_LARGE_DATA?: 'true' | 'false'
|
||||||
|
SINGLE_FILE_BUILD?: 'true' | 'false'
|
||||||
|
WS_PORT?: string
|
||||||
|
DISABLE_SERVICE_WORKER?: 'true' | 'false'
|
||||||
|
CONFIG_JSON_SOURCE?: 'BUNDLED' | 'REMOTE'
|
||||||
|
LOCAL_CONFIG_FILE?: string
|
||||||
|
BUILD_VERSION?: string
|
||||||
|
|
||||||
|
// Build internals
|
||||||
|
GITHUB_REPOSITORY?: string
|
||||||
|
VERCEL_GIT_REPO_OWNER?: string
|
||||||
|
VERCEL_GIT_REPO_SLUG?: string
|
||||||
|
|
||||||
|
// UI
|
||||||
|
MAIN_MENU_LINKS?: string
|
||||||
|
ALWAYS_MINIMAL_SERVER_UI?: 'true' | 'false'
|
||||||
|
|
||||||
|
// App features
|
||||||
|
ENABLE_COOKIE_STORAGE?: string
|
||||||
|
COOKIE_STORAGE_PREFIX?: string
|
||||||
|
|
||||||
|
// Build info. Release information
|
||||||
|
RELEASE_TAG?: string
|
||||||
|
RELEASE_LINK?: string
|
||||||
|
RELEASE_CHANGELOG?: string
|
||||||
|
|
||||||
|
// Build info
|
||||||
|
INLINED_APP_CONFIG?: string
|
||||||
|
GITHUB_URL?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -46,6 +46,8 @@ export const showModal = (elem: /* (HTMLElement & Record<string, any>) | */{ re
|
||||||
activeModalStack.push(resolved)
|
activeModalStack.push(resolved)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.showModal = showModal
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @returns true if previous modal was restored
|
* @returns true if previous modal was restored
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ window.bot = undefined
|
||||||
window.THREE = undefined
|
window.THREE = undefined
|
||||||
window.localServer = undefined
|
window.localServer = undefined
|
||||||
window.worldView = undefined
|
window.worldView = undefined
|
||||||
window.viewer = undefined
|
window.viewer = undefined // legacy
|
||||||
|
window.appViewer = undefined
|
||||||
window.loadedData = undefined
|
window.loadedData = undefined
|
||||||
window.customEvents = new EventEmitter()
|
window.customEvents = new EventEmitter()
|
||||||
window.customEvents.setMaxListeners(10_000)
|
window.customEvents.setMaxListeners(10_000)
|
||||||
|
|
|
||||||
333
src/index.ts
333
src/index.ts
|
|
@ -29,7 +29,7 @@ import './reactUi'
|
||||||
import { lockUrl, onBotCreate } from './controls'
|
import { lockUrl, onBotCreate } from './controls'
|
||||||
import './dragndrop'
|
import './dragndrop'
|
||||||
import { possiblyCleanHandle } from './browserfs'
|
import { possiblyCleanHandle } from './browserfs'
|
||||||
import downloadAndOpenFile from './downloadAndOpenFile'
|
import downloadAndOpenFile, { isInterestedInDownload } from './downloadAndOpenFile'
|
||||||
|
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import net, { Socket } from 'net'
|
import net, { Socket } from 'net'
|
||||||
|
|
@ -56,13 +56,12 @@ import { isCypress } from './standaloneUtils'
|
||||||
|
|
||||||
import { startLocalServer, unsupportedLocalServerFeatures } from './createLocalServer'
|
import { startLocalServer, unsupportedLocalServerFeatures } from './createLocalServer'
|
||||||
import defaultServerOptions from './defaultLocalServerOptions'
|
import defaultServerOptions from './defaultLocalServerOptions'
|
||||||
import dayCycle from './dayCycle'
|
|
||||||
|
|
||||||
import { onAppLoad, resourcepackReload, resourcePackState } from './resourcePack'
|
import { onAppLoad, resourcepackReload, resourcePackState } from './resourcePack'
|
||||||
import { ConnectPeerOptions, connectToPeer } from './localServerMultiplayer'
|
import { ConnectPeerOptions, connectToPeer } from './localServerMultiplayer'
|
||||||
import CustomChannelClient from './customClient'
|
import CustomChannelClient from './customClient'
|
||||||
import { registerServiceWorker } from './serviceWorker'
|
import { registerServiceWorker } from './serviceWorker'
|
||||||
import { appStatusState, lastConnectOptions } from './react/AppStatusProvider'
|
import { appStatusState, lastConnectOptions, quickDevReconnect } from './react/AppStatusProvider'
|
||||||
|
|
||||||
import { fsState } from './loadSave'
|
import { fsState } from './loadSave'
|
||||||
import { watchFov } from './rendererUtils'
|
import { watchFov } from './rendererUtils'
|
||||||
|
|
@ -74,7 +73,7 @@ import { showNotification } from './react/NotificationProvider'
|
||||||
import { saveToBrowserMemory } from './react/PauseScreen'
|
import { saveToBrowserMemory } from './react/PauseScreen'
|
||||||
import './devReload'
|
import './devReload'
|
||||||
import './water'
|
import './water'
|
||||||
import { ConnectOptions, getVersionAutoSelect, downloadOtherGameData, downloadAllMinecraftData } from './connect'
|
import { ConnectOptions, getVersionAutoSelect, downloadOtherGameData, downloadAllMinecraftData, loadMinecraftData } from './connect'
|
||||||
import { ref, subscribe } from 'valtio'
|
import { ref, subscribe } from 'valtio'
|
||||||
import { signInMessageState } from './react/SignInMessageProvider'
|
import { signInMessageState } from './react/SignInMessageProvider'
|
||||||
import { findServerPassword, updateAuthenticatedAccountData, updateLoadedServerData, updateServerConnectionHistory } from './react/serversStorage'
|
import { findServerPassword, updateAuthenticatedAccountData, updateLoadedServerData, updateServerConnectionHistory } from './react/serversStorage'
|
||||||
|
|
@ -97,6 +96,7 @@ import { registerOpenBenchmarkListener } from './benchmark'
|
||||||
import { tryHandleBuiltinCommand } from './builtinCommands'
|
import { tryHandleBuiltinCommand } from './builtinCommands'
|
||||||
import { loadingTimerState } from './react/LoadingTimer'
|
import { loadingTimerState } from './react/LoadingTimer'
|
||||||
import { loadPluginsIntoWorld } from './react/CreateWorldProvider'
|
import { loadPluginsIntoWorld } from './react/CreateWorldProvider'
|
||||||
|
import { getCurrentProxy, getCurrentUsername } from './react/ServersList'
|
||||||
|
|
||||||
window.debug = debug
|
window.debug = debug
|
||||||
window.beforeRenderFrame = []
|
window.beforeRenderFrame = []
|
||||||
|
|
@ -166,6 +166,7 @@ export async function connect (connectOptions: ConnectOptions) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appStatusState.showReconnect = false
|
||||||
loadingTimerState.loading = true
|
loadingTimerState.loading = true
|
||||||
loadingTimerState.start = Date.now()
|
loadingTimerState.start = Date.now()
|
||||||
miscUiState.hasErrors = false
|
miscUiState.hasErrors = false
|
||||||
|
|
@ -209,12 +210,17 @@ export async function connect (connectOptions: ConnectOptions) {
|
||||||
|
|
||||||
let ended = false
|
let ended = false
|
||||||
let bot!: typeof __type_bot
|
let bot!: typeof __type_bot
|
||||||
|
let hadConnected = false
|
||||||
const destroyAll = (wasKicked = false) => {
|
const destroyAll = (wasKicked = false) => {
|
||||||
if (ended) return
|
if (ended) return
|
||||||
loadingTimerState.loading = false
|
loadingTimerState.loading = false
|
||||||
const hadConnected = !!bot
|
const { alwaysReconnect } = appQueryParams
|
||||||
if (!wasKicked && miscUiState.appConfig?.allowAutoConnect && appQueryParams.autoConnect && hadConnected) {
|
if ((!wasKicked && miscUiState.appConfig?.allowAutoConnect && appQueryParams.autoConnect && hadConnected) || (alwaysReconnect)) {
|
||||||
location.reload()
|
if (alwaysReconnect === 'quick' || alwaysReconnect === 'fast') {
|
||||||
|
quickDevReconnect()
|
||||||
|
} else {
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
errorAbortController.abort()
|
errorAbortController.abort()
|
||||||
ended = true
|
ended = true
|
||||||
|
|
@ -229,8 +235,12 @@ export async function connect (connectOptions: ConnectOptions) {
|
||||||
bot.emit('end', '')
|
bot.emit('end', '')
|
||||||
bot.removeAllListeners()
|
bot.removeAllListeners()
|
||||||
bot._client.removeAllListeners()
|
bot._client.removeAllListeners()
|
||||||
//@ts-expect-error TODO?
|
bot._client = {
|
||||||
bot._client = undefined
|
//@ts-expect-error
|
||||||
|
write (packetName) {
|
||||||
|
console.warn('Tried to write packet', packetName, 'after bot was destroyed')
|
||||||
|
}
|
||||||
|
}
|
||||||
//@ts-expect-error
|
//@ts-expect-error
|
||||||
window.bot = bot = undefined
|
window.bot = bot = undefined
|
||||||
}
|
}
|
||||||
|
|
@ -276,6 +286,10 @@ export async function connect (connectOptions: ConnectOptions) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (e.reason?.stack?.includes('chrome-extension://')) {
|
||||||
|
// ignore issues caused by chrome extension
|
||||||
|
return
|
||||||
|
}
|
||||||
handleError(e.reason)
|
handleError(e.reason)
|
||||||
}, {
|
}, {
|
||||||
signal: errorAbortController.signal
|
signal: errorAbortController.signal
|
||||||
|
|
@ -290,7 +304,7 @@ export async function connect (connectOptions: ConnectOptions) {
|
||||||
|
|
||||||
if (connectOptions.server && !connectOptions.viewerWsConnect && !parsedServer.isWebSocket) {
|
if (connectOptions.server && !connectOptions.viewerWsConnect && !parsedServer.isWebSocket) {
|
||||||
console.log(`using proxy ${proxy.host}:${proxy.port || location.port}`)
|
console.log(`using proxy ${proxy.host}:${proxy.port || location.port}`)
|
||||||
net['setProxy']({ hostname: proxy.host, port: proxy.port, headers: { Authorization: `Bearer ${new URLSearchParams(location.search).get('token') ?? ''}` } })
|
net['setProxy']({ hostname: proxy.host, port: proxy.port, headers: { Authorization: `Bearer ${new URLSearchParams(location.search).get('token') ?? ''}` }, artificialDelay: appQueryParams.addPing ? Number(appQueryParams.addPing) : undefined })
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderDistance = singleplayer ? renderDistanceSingleplayer : multiplayerRenderDistance
|
const renderDistance = singleplayer ? renderDistanceSingleplayer : multiplayerRenderDistance
|
||||||
|
|
@ -331,6 +345,7 @@ export async function connect (connectOptions: ConnectOptions) {
|
||||||
await progress.executeWithMessage(
|
await progress.executeWithMessage(
|
||||||
'Processing downloaded Minecraft data',
|
'Processing downloaded Minecraft data',
|
||||||
async () => {
|
async () => {
|
||||||
|
await loadMinecraftData(version)
|
||||||
await appViewer.resourcesManager.loadSourceData(version)
|
await appViewer.resourcesManager.loadSourceData(version)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -448,17 +463,20 @@ export async function connect (connectOptions: ConnectOptions) {
|
||||||
|
|
||||||
let newTokensCacheResult = null as any
|
let newTokensCacheResult = null as any
|
||||||
const cachedTokens = typeof connectOptions.authenticatedAccount === 'object' ? connectOptions.authenticatedAccount.cachedTokens : {}
|
const cachedTokens = typeof connectOptions.authenticatedAccount === 'object' ? connectOptions.authenticatedAccount.cachedTokens : {}
|
||||||
const authData = connectOptions.authenticatedAccount ? await microsoftAuthflow({
|
let authData: Awaited<ReturnType<typeof microsoftAuthflow>> | undefined
|
||||||
tokenCaches: cachedTokens,
|
if (connectOptions.authenticatedAccount) {
|
||||||
proxyBaseUrl: connectOptions.proxy,
|
authData = await microsoftAuthflow({
|
||||||
setProgressText (text) {
|
tokenCaches: cachedTokens,
|
||||||
progress.setMessage(text)
|
proxyBaseUrl: connectOptions.proxy,
|
||||||
},
|
setProgressText (text) {
|
||||||
setCacheResult (result) {
|
progress.setMessage(text)
|
||||||
newTokensCacheResult = result
|
},
|
||||||
},
|
setCacheResult (result) {
|
||||||
connectingServer: server.host
|
newTokensCacheResult = result
|
||||||
}) : undefined
|
},
|
||||||
|
connectingServer: server.host
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (p2pMultiplayer) {
|
if (p2pMultiplayer) {
|
||||||
clientDataStream = await connectToPeer(connectOptions.peerId!, connectOptions.peerOptions)
|
clientDataStream = await connectToPeer(connectOptions.peerId!, connectOptions.peerOptions)
|
||||||
|
|
@ -569,6 +587,7 @@ export async function connect (connectOptions: ConnectOptions) {
|
||||||
// "mapDownloader-saveInternal": false, // do not save into memory, todo must be implemeneted as we do really care of ram
|
// "mapDownloader-saveInternal": false, // do not save into memory, todo must be implemeneted as we do really care of ram
|
||||||
}) as unknown as typeof __type_bot
|
}) as unknown as typeof __type_bot
|
||||||
window.bot = bot
|
window.bot = bot
|
||||||
|
|
||||||
if (connectOptions.viewerWsConnect) {
|
if (connectOptions.viewerWsConnect) {
|
||||||
void onBotCreatedViewerHandler()
|
void onBotCreatedViewerHandler()
|
||||||
}
|
}
|
||||||
|
|
@ -691,6 +710,7 @@ export async function connect (connectOptions: ConnectOptions) {
|
||||||
onBotCreate()
|
onBotCreate()
|
||||||
|
|
||||||
bot.once('login', () => {
|
bot.once('login', () => {
|
||||||
|
errorAbortController.abort()
|
||||||
loadingTimerState.networkOnlyStart = 0
|
loadingTimerState.networkOnlyStart = 0
|
||||||
progress.setMessage('Loading world')
|
progress.setMessage('Loading world')
|
||||||
})
|
})
|
||||||
|
|
@ -708,7 +728,7 @@ export async function connect (connectOptions: ConnectOptions) {
|
||||||
resolve()
|
resolve()
|
||||||
unsub()
|
unsub()
|
||||||
} else {
|
} else {
|
||||||
const perc = Math.round(appViewer.rendererState.world.chunksLoaded.size / appViewer.rendererState.world.chunksTotalNumber * 100)
|
const perc = Math.round(appViewer.rendererState.world.chunksLoaded.size / appViewer.nonReactiveState.world.chunksTotalNumber * 100)
|
||||||
progress?.reportProgress('chunks', perc / 100)
|
progress?.reportProgress('chunks', perc / 100)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -727,9 +747,12 @@ export async function connect (connectOptions: ConnectOptions) {
|
||||||
})
|
})
|
||||||
await appViewer.resourcesManager.promiseAssetsReady
|
await appViewer.resourcesManager.promiseAssetsReady
|
||||||
}
|
}
|
||||||
errorAbortController.abort()
|
|
||||||
if (appStatusState.isError) return
|
if (appStatusState.isError) return
|
||||||
|
|
||||||
|
if (!appViewer.resourcesManager.currentResources?.itemsRenderer) {
|
||||||
|
await appViewer.resourcesManager.updateAssetsData({})
|
||||||
|
}
|
||||||
|
|
||||||
const loadWorldStart = Date.now()
|
const loadWorldStart = Date.now()
|
||||||
console.log('try to focus window')
|
console.log('try to focus window')
|
||||||
window.focus?.()
|
window.focus?.()
|
||||||
|
|
@ -741,7 +764,7 @@ export async function connect (connectOptions: ConnectOptions) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (p2pConnectTimeout) clearTimeout(p2pConnectTimeout)
|
if (p2pConnectTimeout) clearTimeout(p2pConnectTimeout)
|
||||||
playerState.onlineMode = !!connectOptions.authenticatedAccount
|
playerState.reactive.onlineMode = !!connectOptions.authenticatedAccount
|
||||||
|
|
||||||
progress.setMessage('Placing blocks (starting viewer)')
|
progress.setMessage('Placing blocks (starting viewer)')
|
||||||
if (!connectOptions.worldStateFileContents || connectOptions.worldStateFileContents.length < 3 * 1024 * 1024) {
|
if (!connectOptions.worldStateFileContents || connectOptions.worldStateFileContents.length < 3 * 1024 * 1024) {
|
||||||
|
|
@ -765,9 +788,11 @@ export async function connect (connectOptions: ConnectOptions) {
|
||||||
console.log('bot spawned - starting viewer')
|
console.log('bot spawned - starting viewer')
|
||||||
await appViewer.startWorld(bot.world, renderDistance)
|
await appViewer.startWorld(bot.world, renderDistance)
|
||||||
appViewer.worldView!.listenToBot(bot)
|
appViewer.worldView!.listenToBot(bot)
|
||||||
|
if (appViewer.backend) {
|
||||||
|
void appViewer.worldView!.init(bot.entity.position)
|
||||||
|
}
|
||||||
|
|
||||||
initMotionTracking()
|
initMotionTracking()
|
||||||
dayCycle()
|
|
||||||
|
|
||||||
// Bot position callback
|
// Bot position callback
|
||||||
const botPosition = () => {
|
const botPosition = () => {
|
||||||
|
|
@ -819,11 +844,32 @@ export async function connect (connectOptions: ConnectOptions) {
|
||||||
miscUiState.gameLoaded = true
|
miscUiState.gameLoaded = true
|
||||||
miscUiState.loadedServerIndex = connectOptions.serverIndex ?? ''
|
miscUiState.loadedServerIndex = connectOptions.serverIndex ?? ''
|
||||||
customEvents.emit('gameLoaded')
|
customEvents.emit('gameLoaded')
|
||||||
|
|
||||||
|
// Test iOS Safari crash by creating memory pressure
|
||||||
|
if (appQueryParams.testIosCrash) {
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('Starting iOS crash test with memory pressure...')
|
||||||
|
// eslint-disable-next-line sonarjs/no-unused-collection
|
||||||
|
const arrays: number[][] = []
|
||||||
|
try {
|
||||||
|
// Create large arrays until we run out of memory
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
|
while (true) {
|
||||||
|
const arr = Array.from({ length: 1024 * 1024 }).fill(0).map((_, i) => i)
|
||||||
|
arrays.push(arr)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Memory allocation failed:', e)
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
progress.end()
|
progress.end()
|
||||||
setLoadingScreenStatus(undefined)
|
setLoadingScreenStatus(undefined)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
handleError(err)
|
handleError(err)
|
||||||
}
|
}
|
||||||
|
hadConnected = true
|
||||||
}
|
}
|
||||||
// don't use spawn event, player can be dead
|
// don't use spawn event, player can be dead
|
||||||
bot.once(spawnEarlier ? 'forcedMove' : 'health', displayWorld)
|
bot.once(spawnEarlier ? 'forcedMove' : 'health', displayWorld)
|
||||||
|
|
@ -847,37 +893,7 @@ export async function connect (connectOptions: ConnectOptions) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const reconnectOptions = sessionStorage.getItem('reconnectOptions') ? JSON.parse(sessionStorage.getItem('reconnectOptions')!) : undefined
|
|
||||||
|
|
||||||
listenGlobalEvents()
|
listenGlobalEvents()
|
||||||
const unsubscribe = subscribe(miscUiState, async () => {
|
|
||||||
if (miscUiState.fsReady && miscUiState.appConfig) {
|
|
||||||
unsubscribe()
|
|
||||||
if (reconnectOptions) {
|
|
||||||
sessionStorage.removeItem('reconnectOptions')
|
|
||||||
if (Date.now() - reconnectOptions.timestamp < 1000 * 60 * 2) {
|
|
||||||
void connect(reconnectOptions.value)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (appQueryParams.singleplayer === '1' || appQueryParams.sp === '1') {
|
|
||||||
loadSingleplayer({}, {
|
|
||||||
worldFolder: undefined,
|
|
||||||
...appQueryParams.version ? { version: appQueryParams.version } : {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (appQueryParams.loadSave) {
|
|
||||||
const savePath = `/data/worlds/${appQueryParams.loadSave}`
|
|
||||||
try {
|
|
||||||
await fs.promises.stat(savePath)
|
|
||||||
} catch (err) {
|
|
||||||
alert(`Save ${savePath} not found`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await loadInMemorySave(savePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// #region fire click event on touch as we disable default behaviors
|
// #region fire click event on touch as we disable default behaviors
|
||||||
let activeTouch: { touch: Touch, elem: HTMLElement, start: number } | undefined
|
let activeTouch: { touch: Touch, elem: HTMLElement, start: number } | undefined
|
||||||
|
|
@ -913,90 +929,148 @@ document.body.addEventListener('touchstart', (e) => {
|
||||||
}, { passive: false })
|
}, { passive: false })
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// qs open actions
|
// immediate game enter actions: reconnect or URL QS
|
||||||
if (!reconnectOptions) {
|
const maybeEnterGame = () => {
|
||||||
downloadAndOpenFile().then((downloadAction) => {
|
const waitForConfigFsLoad = (fn: () => void) => {
|
||||||
if (downloadAction) return
|
let unsubscribe: () => void | undefined
|
||||||
if (appQueryParams.reconnect && process.env.NODE_ENV === 'development') {
|
const checkDone = () => {
|
||||||
const lastConnect = JSON.parse(localStorage.lastConnectOptions ?? {})
|
if (miscUiState.fsReady && miscUiState.appConfig) {
|
||||||
|
fn()
|
||||||
|
unsubscribe?.()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checkDone()) {
|
||||||
|
const text = miscUiState.appConfig ? 'Loading' : 'Loading config'
|
||||||
|
setLoadingScreenStatus(text)
|
||||||
|
unsubscribe = subscribe(miscUiState, checkDone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reconnectOptions = sessionStorage.getItem('reconnectOptions') ? JSON.parse(sessionStorage.getItem('reconnectOptions')!) : undefined
|
||||||
|
|
||||||
|
if (reconnectOptions) {
|
||||||
|
sessionStorage.removeItem('reconnectOptions')
|
||||||
|
if (Date.now() - reconnectOptions.timestamp < 1000 * 60 * 2) {
|
||||||
|
return waitForConfigFsLoad(async () => {
|
||||||
|
void connect(reconnectOptions.value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appQueryParams.reconnect && localStorage.lastConnectOptions && process.env.NODE_ENV === 'development') {
|
||||||
|
const lastConnect = JSON.parse(localStorage.lastConnectOptions ?? {})
|
||||||
|
return waitForConfigFsLoad(async () => {
|
||||||
void connect({
|
void connect({
|
||||||
botVersion: appQueryParams.version ?? undefined,
|
botVersion: appQueryParams.version ?? undefined,
|
||||||
...lastConnect,
|
...lastConnect,
|
||||||
ip: appQueryParams.ip || undefined
|
ip: appQueryParams.ip || undefined
|
||||||
})
|
})
|
||||||
return
|
|
||||||
}
|
|
||||||
if (appQueryParams.ip || appQueryParams.proxy) {
|
|
||||||
const waitAppConfigLoad = !appQueryParams.proxy
|
|
||||||
const openServerEditor = () => {
|
|
||||||
hideModal()
|
|
||||||
if (appQueryParams.onlyConnect) {
|
|
||||||
showModal({ reactType: 'only-connect-server' })
|
|
||||||
} else {
|
|
||||||
showModal({ reactType: 'editServer' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
showModal({ reactType: 'empty' })
|
|
||||||
if (waitAppConfigLoad) {
|
|
||||||
const unsubscribe = subscribe(miscUiState, checkCanDisplay)
|
|
||||||
checkCanDisplay()
|
|
||||||
// eslint-disable-next-line no-inner-declarations
|
|
||||||
function checkCanDisplay () {
|
|
||||||
if (miscUiState.appConfig) {
|
|
||||||
unsubscribe()
|
|
||||||
openServerEditor()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
openServerEditor()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Promise.resolve().then(() => {
|
|
||||||
// try to connect to peer
|
|
||||||
const peerId = appQueryParams.connectPeer
|
|
||||||
const peerOptions = {} as ConnectPeerOptions
|
|
||||||
if (appQueryParams.server) {
|
|
||||||
peerOptions.server = appQueryParams.server
|
|
||||||
}
|
|
||||||
const version = appQueryParams.peerVersion
|
|
||||||
if (peerId) {
|
|
||||||
let username: string | null = options.guestUsername
|
|
||||||
if (options.askGuestName) username = prompt('Enter your username', username)
|
|
||||||
if (!username) return
|
|
||||||
options.guestUsername = username
|
|
||||||
void connect({
|
|
||||||
username,
|
|
||||||
botVersion: version || undefined,
|
|
||||||
peerId,
|
|
||||||
peerOptions
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (appQueryParams.serversList) {
|
if (appQueryParams.singleplayer === '1' || appQueryParams.sp === '1') {
|
||||||
showModal({ reactType: 'serversList' })
|
return waitForConfigFsLoad(async () => {
|
||||||
}
|
loadSingleplayer({}, {
|
||||||
|
worldFolder: undefined,
|
||||||
const viewerWsConnect = appQueryParams.viewerConnect
|
...appQueryParams.version ? { version: appQueryParams.version } : {}
|
||||||
if (viewerWsConnect) {
|
|
||||||
void connect({
|
|
||||||
username: `viewer-${Math.random().toString(36).slice(2, 10)}`,
|
|
||||||
viewerWsConnect,
|
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
|
}
|
||||||
if (appQueryParams.modal) {
|
if (appQueryParams.loadSave) {
|
||||||
const modals = appQueryParams.modal.split(',')
|
const enterSave = async () => {
|
||||||
for (const modal of modals) {
|
const savePath = `/data/worlds/${appQueryParams.loadSave}`
|
||||||
showModal({ reactType: modal })
|
try {
|
||||||
|
await fs.promises.stat(savePath)
|
||||||
|
await loadInMemorySave(savePath)
|
||||||
|
} catch (err) {
|
||||||
|
alert(`Save ${savePath} not found`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, (err) => {
|
return waitForConfigFsLoad(enterSave)
|
||||||
console.error(err)
|
}
|
||||||
alert(`Something went wrong: ${err}`)
|
|
||||||
})
|
if (appQueryParams.ip || appQueryParams.proxy) {
|
||||||
|
const openServerAction = () => {
|
||||||
|
if (appQueryParams.autoConnect && miscUiState.appConfig?.allowAutoConnect) {
|
||||||
|
void connect({
|
||||||
|
server: appQueryParams.ip,
|
||||||
|
proxy: getCurrentProxy(),
|
||||||
|
botVersion: appQueryParams.version ?? undefined,
|
||||||
|
username: getCurrentUsername()!,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingScreenStatus(undefined)
|
||||||
|
if (appQueryParams.onlyConnect || process.env.ALWAYS_MINIMAL_SERVER_UI === 'true') {
|
||||||
|
showModal({ reactType: 'only-connect-server' })
|
||||||
|
} else {
|
||||||
|
showModal({ reactType: 'editServer' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// showModal({ reactType: 'empty' })
|
||||||
|
return waitForConfigFsLoad(openServerAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appQueryParams.connectPeer) {
|
||||||
|
// try to connect to peer
|
||||||
|
const peerId = appQueryParams.connectPeer
|
||||||
|
const peerOptions = {} as ConnectPeerOptions
|
||||||
|
if (appQueryParams.server) {
|
||||||
|
peerOptions.server = appQueryParams.server
|
||||||
|
}
|
||||||
|
const version = appQueryParams.peerVersion
|
||||||
|
let username: string | null = options.guestUsername
|
||||||
|
if (options.askGuestName) username = prompt('Enter your username to connect to peer', username)
|
||||||
|
if (!username) return
|
||||||
|
options.guestUsername = username
|
||||||
|
void connect({
|
||||||
|
username,
|
||||||
|
botVersion: version || undefined,
|
||||||
|
peerId,
|
||||||
|
peerOptions
|
||||||
|
})
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appQueryParams.viewerConnect) {
|
||||||
|
void connect({
|
||||||
|
username: `viewer-${Math.random().toString(36).slice(2, 10)}`,
|
||||||
|
viewerWsConnect: appQueryParams.viewerConnect,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appQueryParams.modal) {
|
||||||
|
const modals = appQueryParams.modal.split(',')
|
||||||
|
for (const modal of modals) {
|
||||||
|
showModal({ reactType: modal })
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appQueryParams.serversList && !miscUiState.appConfig?.appParams?.serversList) {
|
||||||
|
// open UI only if it's in URL
|
||||||
|
showModal({ reactType: 'serversList' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInterestedInDownload()) {
|
||||||
|
void downloadAndOpenFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
void possiblyHandleStateVariable()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
maybeEnterGame()
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
alert(`Something went wrong: ${err}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||||
|
|
@ -1007,6 +1081,5 @@ if (initialLoader) {
|
||||||
}
|
}
|
||||||
window.pageLoaded = true
|
window.pageLoaded = true
|
||||||
|
|
||||||
void possiblyHandleStateVariable()
|
|
||||||
appViewer.waitBackendLoadPromises.push(appStartup())
|
appViewer.waitBackendLoadPromises.push(appStartup())
|
||||||
registerOpenBenchmarkListener()
|
registerOpenBenchmarkListener()
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,10 @@ import PItem, { Item } from 'prismarine-item'
|
||||||
import { versionToNumber } from 'renderer/viewer/common/utils'
|
import { versionToNumber } from 'renderer/viewer/common/utils'
|
||||||
import { getRenamedData } from 'flying-squid/dist/blockRenames'
|
import { getRenamedData } from 'flying-squid/dist/blockRenames'
|
||||||
import PrismarineChatLoader from 'prismarine-chat'
|
import PrismarineChatLoader from 'prismarine-chat'
|
||||||
|
import * as nbt from 'prismarine-nbt'
|
||||||
import { BlockModel } from 'mc-assets'
|
import { BlockModel } from 'mc-assets'
|
||||||
import { activeGuiAtlas } from 'renderer/viewer/lib/guiRenderer'
|
import { renderSlot } from 'renderer/viewer/three/renderSlot'
|
||||||
|
import { loadSkinFromUsername } from 'renderer/viewer/lib/utils/skins'
|
||||||
import Generic95 from '../assets/generic_95.png'
|
import Generic95 from '../assets/generic_95.png'
|
||||||
import { appReplacableResources } from './generated/resources'
|
import { appReplacableResources } from './generated/resources'
|
||||||
import { activeModalStack, hideCurrentModal, hideModal, miscUiState, showModal } from './globalState'
|
import { activeModalStack, hideCurrentModal, hideModal, miscUiState, showModal } from './globalState'
|
||||||
|
|
@ -21,8 +23,10 @@ import { currentScaling } from './scaleInterface'
|
||||||
import { getItemDescription } from './itemsDescriptions'
|
import { getItemDescription } from './itemsDescriptions'
|
||||||
import { MessageFormatPart } from './chatUtils'
|
import { MessageFormatPart } from './chatUtils'
|
||||||
import { GeneralInputItem, getItemMetadata, getItemModelName, getItemNameRaw, RenderItem } from './mineflayer/items'
|
import { GeneralInputItem, getItemMetadata, getItemModelName, getItemNameRaw, RenderItem } from './mineflayer/items'
|
||||||
|
import { playerState } from './mineflayer/playerState'
|
||||||
|
import { modelViewerState } from './react/OverlayModelViewer'
|
||||||
|
|
||||||
const loadedImagesCache = new Map<string, HTMLImageElement>()
|
const loadedImagesCache = new Map<string, HTMLImageElement | ImageBitmap>()
|
||||||
const cleanLoadedImagesCache = () => {
|
const cleanLoadedImagesCache = () => {
|
||||||
loadedImagesCache.delete('blocks')
|
loadedImagesCache.delete('blocks')
|
||||||
loadedImagesCache.delete('items')
|
loadedImagesCache.delete('items')
|
||||||
|
|
@ -38,6 +42,34 @@ export const jeiCustomCategories = proxy({
|
||||||
value: [] as Array<{ id: string, categoryTitle: string, items: any[] }>
|
value: [] as Array<{ id: string, categoryTitle: string, items: any[] }>
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let remotePlayerSkin: string | undefined | Promise<string>
|
||||||
|
|
||||||
|
export const showInventoryPlayer = () => {
|
||||||
|
modelViewerState.model = {
|
||||||
|
positioning: {
|
||||||
|
windowWidth: 176,
|
||||||
|
windowHeight: 166,
|
||||||
|
x: 25,
|
||||||
|
y: 8,
|
||||||
|
width: 50,
|
||||||
|
height: 70,
|
||||||
|
scaled: true,
|
||||||
|
onlyInitialScale: true,
|
||||||
|
followCursor: true,
|
||||||
|
},
|
||||||
|
// models: ['https://bucket.mcraft.fun/sitarbuckss.glb'],
|
||||||
|
// debug: true,
|
||||||
|
steveModelSkin: appViewer.playerState.reactive.playerSkin ?? (typeof remotePlayerSkin === 'string' ? remotePlayerSkin : ''),
|
||||||
|
}
|
||||||
|
if (remotePlayerSkin === undefined && !appViewer.playerState.reactive.playerSkin) {
|
||||||
|
remotePlayerSkin = loadSkinFromUsername(bot.username, 'skin').then(a => {
|
||||||
|
setTimeout(() => { showInventoryPlayer() }, 0) // todo patch instead and make reactive
|
||||||
|
remotePlayerSkin = a ?? ''
|
||||||
|
return remotePlayerSkin
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const onGameLoad = () => {
|
export const onGameLoad = () => {
|
||||||
version = bot.version
|
version = bot.version
|
||||||
|
|
||||||
|
|
@ -55,12 +87,23 @@ export const onGameLoad = () => {
|
||||||
return type
|
return type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maybeParseNbtJson = (data: any) => {
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
try {
|
||||||
|
data = JSON.parse(data)
|
||||||
|
} catch (err) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nbt.simplify(data) ?? data
|
||||||
|
}
|
||||||
|
|
||||||
bot.on('windowOpen', (win) => {
|
bot.on('windowOpen', (win) => {
|
||||||
const implementedWindow = implementedContainersGuiMap[mapWindowType(win.type as string, win.inventoryStart)]
|
const implementedWindow = implementedContainersGuiMap[mapWindowType(win.type as string, win.inventoryStart)]
|
||||||
if (implementedWindow) {
|
if (implementedWindow) {
|
||||||
openWindow(implementedWindow)
|
openWindow(implementedWindow, maybeParseNbtJson(win.title))
|
||||||
} else if (options.unimplementedContainers) {
|
} else if (options.unimplementedContainers) {
|
||||||
openWindow('ChestWin')
|
openWindow('ChestWin', maybeParseNbtJson(win.title))
|
||||||
} else {
|
} else {
|
||||||
// todo format
|
// todo format
|
||||||
displayClientChat(`[client error] cannot open unimplemented window ${win.id} (${win.type}). Slots: ${win.slots.map(item => getItemName(item)).filter(Boolean).join(', ')}`)
|
displayClientChat(`[client error] cannot open unimplemented window ${win.id} (${win.type}). Slots: ${win.slots.map(item => getItemName(item)).filter(Boolean).join(', ')}`)
|
||||||
|
|
@ -131,11 +174,12 @@ export const onGameLoad = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getImageSrc = (path): string | HTMLImageElement => {
|
const getImageSrc = (path): string | HTMLImageElement | ImageBitmap => {
|
||||||
switch (path) {
|
switch (path) {
|
||||||
case 'gui/container/inventory': return appReplacableResources.latest_gui_container_inventory.content
|
case 'gui/container/inventory': return appReplacableResources.latest_gui_container_inventory.content
|
||||||
case 'blocks': return appViewer.resourcesManager.currentResources!.blocksAtlasParser.latestImage
|
case 'blocks': return appViewer.resourcesManager.blocksAtlasParser.latestImage
|
||||||
case 'items': return appViewer.resourcesManager.currentResources!.itemsAtlasParser.latestImage
|
case 'items': return appViewer.resourcesManager.itemsAtlasParser.latestImage
|
||||||
|
case 'gui': return appViewer.resourcesManager.currentResources!.guiAtlas!.image
|
||||||
case 'gui/container/dispenser': return appReplacableResources.latest_gui_container_dispenser.content
|
case 'gui/container/dispenser': return appReplacableResources.latest_gui_container_dispenser.content
|
||||||
case 'gui/container/furnace': return appReplacableResources.latest_gui_container_furnace.content
|
case 'gui/container/furnace': return appReplacableResources.latest_gui_container_furnace.content
|
||||||
case 'gui/container/crafting_table': return appReplacableResources.latest_gui_container_crafting_table.content
|
case 'gui/container/crafting_table': return appReplacableResources.latest_gui_container_crafting_table.content
|
||||||
|
|
@ -158,12 +202,20 @@ const getImage = ({ path = undefined as string | undefined, texture = undefined
|
||||||
if (image) {
|
if (image) {
|
||||||
return image
|
return image
|
||||||
}
|
}
|
||||||
if (!path && !texture) throw new Error('Either pass path or texture')
|
if (!path && !texture) {
|
||||||
|
throw new Error('Either pass path or texture')
|
||||||
|
}
|
||||||
const loadPath = (blockData ? 'blocks' : path ?? texture)!
|
const loadPath = (blockData ? 'blocks' : path ?? texture)!
|
||||||
if (loadedImagesCache.has(loadPath)) {
|
if (loadedImagesCache.has(loadPath)) {
|
||||||
onLoad()
|
onLoad()
|
||||||
} else {
|
} else {
|
||||||
const imageSrc = getImageSrc(loadPath)
|
const imageSrc = getImageSrc(loadPath)
|
||||||
|
if (imageSrc instanceof ImageBitmap) {
|
||||||
|
onLoad()
|
||||||
|
loadedImagesCache.set(loadPath, imageSrc)
|
||||||
|
return imageSrc
|
||||||
|
}
|
||||||
|
|
||||||
let image: HTMLImageElement
|
let image: HTMLImageElement
|
||||||
if (imageSrc instanceof Image) {
|
if (imageSrc instanceof Image) {
|
||||||
image = imageSrc
|
image = imageSrc
|
||||||
|
|
@ -177,79 +229,6 @@ const getImage = ({ path = undefined as string | undefined, texture = undefined
|
||||||
return loadedImagesCache.get(loadPath)
|
return loadedImagesCache.get(loadPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ResolvedItemModelRender = {
|
|
||||||
modelName: string,
|
|
||||||
originalItemName?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const renderSlot = (model: ResolvedItemModelRender, debugIsQuickbar = false, fullBlockModelSupport = false): {
|
|
||||||
texture: string,
|
|
||||||
blockData?: Record<string, { slice, path }> & { resolvedModel: BlockModel },
|
|
||||||
scale?: number,
|
|
||||||
slice?: number[],
|
|
||||||
modelName?: string,
|
|
||||||
image?: HTMLImageElement
|
|
||||||
} | undefined => {
|
|
||||||
let itemModelName = model.modelName
|
|
||||||
const isItem = loadedData.itemsByName[itemModelName]
|
|
||||||
|
|
||||||
// #region normalize item name
|
|
||||||
if (versionToNumber(bot.version) < versionToNumber('1.13')) itemModelName = getRenamedData(isItem ? 'items' : 'blocks', itemModelName, bot.version, '1.13.1') as string
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
|
|
||||||
let itemTexture
|
|
||||||
|
|
||||||
if (!fullBlockModelSupport) {
|
|
||||||
const atlas = activeGuiAtlas.atlas?.json
|
|
||||||
// todo atlas holds all rendered blocks, not all possibly rendered item/block models, need to request this on demand instead (this is how vanilla works)
|
|
||||||
const tryGetAtlasTexture = (name?: string) => name && atlas?.textures[name.replace('minecraft:', '').replace('block/', '').replace('blocks/', '').replace('item/', '').replace('items/', '').replace('_inventory', '')]
|
|
||||||
const item = tryGetAtlasTexture(itemModelName) ?? tryGetAtlasTexture(model.originalItemName)
|
|
||||||
if (item) {
|
|
||||||
const x = item.u * atlas.width
|
|
||||||
const y = item.v * atlas.height
|
|
||||||
return {
|
|
||||||
texture: 'gui',
|
|
||||||
image: activeGuiAtlas.atlas!.image,
|
|
||||||
slice: [x, y, atlas.tileSize, atlas.tileSize],
|
|
||||||
scale: 0.25,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const blockToTopTexture = (r) => r.top ?? r
|
|
||||||
|
|
||||||
try {
|
|
||||||
assertDefined(appViewer.resourcesManager.currentResources?.itemsRenderer)
|
|
||||||
itemTexture =
|
|
||||||
appViewer.resourcesManager.currentResources.itemsRenderer.getItemTexture(itemModelName, {}, false, fullBlockModelSupport)
|
|
||||||
?? (model.originalItemName ? appViewer.resourcesManager.currentResources.itemsRenderer.getItemTexture(model.originalItemName, {}, false, fullBlockModelSupport) : undefined)
|
|
||||||
?? appViewer.resourcesManager.currentResources.itemsRenderer.getItemTexture('item/missing_texture')!
|
|
||||||
} catch (err) {
|
|
||||||
inGameError(`Failed to render item ${itemModelName} (original: ${model.originalItemName}) on ${bot.version} (resourcepack: ${options.enabledResourcepack}): ${err.stack}`)
|
|
||||||
itemTexture = blockToTopTexture(appViewer.resourcesManager.currentResources!.itemsRenderer.getItemTexture('errored')!)
|
|
||||||
}
|
|
||||||
|
|
||||||
itemTexture ??= blockToTopTexture(appViewer.resourcesManager.currentResources!.itemsRenderer.getItemTexture('unknown')!)
|
|
||||||
|
|
||||||
|
|
||||||
if ('type' in itemTexture) {
|
|
||||||
// is item
|
|
||||||
return {
|
|
||||||
texture: itemTexture.type,
|
|
||||||
slice: itemTexture.slice,
|
|
||||||
modelName: itemModelName
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// is block
|
|
||||||
return {
|
|
||||||
texture: 'blocks',
|
|
||||||
blockData: itemTexture,
|
|
||||||
modelName: itemModelName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getItemName = (slot: Item | RenderItem | null) => {
|
const getItemName = (slot: Item | RenderItem | null) => {
|
||||||
const parsed = getItemNameRaw(slot, appViewer.resourcesManager)
|
const parsed = getItemNameRaw(slot, appViewer.resourcesManager)
|
||||||
if (!parsed) return
|
if (!parsed) return
|
||||||
|
|
@ -269,10 +248,15 @@ const itemToVisualKey = (slot: RenderItem | Item | null) => {
|
||||||
slot['metadata'],
|
slot['metadata'],
|
||||||
slot.nbt ? JSON.stringify(slot.nbt) : '',
|
slot.nbt ? JSON.stringify(slot.nbt) : '',
|
||||||
slot['components'] ? JSON.stringify(slot['components']) : '',
|
slot['components'] ? JSON.stringify(slot['components']) : '',
|
||||||
activeGuiAtlas.version,
|
appViewer.resourcesManager.currentResources!.guiAtlasVersion,
|
||||||
].join('|')
|
].join('|')
|
||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
const validateSlot = (slot: any, index: number) => {
|
||||||
|
if (!slot.texture) {
|
||||||
|
throw new Error(`Slot has no texture: ${index} ${slot.name}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
const mapSlots = (slots: Array<RenderItem | Item | null>, isJei = false) => {
|
const mapSlots = (slots: Array<RenderItem | Item | null>, isJei = false) => {
|
||||||
const newSlots = slots.map((slot, i) => {
|
const newSlots = slots.map((slot, i) => {
|
||||||
if (!slot) return null
|
if (!slot) return null
|
||||||
|
|
@ -282,6 +266,7 @@ const mapSlots = (slots: Array<RenderItem | Item | null>, isJei = false) => {
|
||||||
const newKey = itemToVisualKey(slot)
|
const newKey = itemToVisualKey(slot)
|
||||||
slot['cacheKey'] = i + '|' + newKey
|
slot['cacheKey'] = i + '|' + newKey
|
||||||
if (oldKey && oldKey === newKey) {
|
if (oldKey && oldKey === newKey) {
|
||||||
|
validateSlot(lastMappedSlots[i], i)
|
||||||
return lastMappedSlots[i]
|
return lastMappedSlots[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -289,8 +274,8 @@ const mapSlots = (slots: Array<RenderItem | Item | null>, isJei = false) => {
|
||||||
try {
|
try {
|
||||||
if (slot.durabilityUsed && slot.maxDurability) slot.durabilityUsed = Math.min(slot.durabilityUsed, slot.maxDurability)
|
if (slot.durabilityUsed && slot.maxDurability) slot.durabilityUsed = Math.min(slot.durabilityUsed, slot.maxDurability)
|
||||||
const debugIsQuickbar = !isJei && i === bot.inventory.hotbarStart + bot.quickBarSlot
|
const debugIsQuickbar = !isJei && i === bot.inventory.hotbarStart + bot.quickBarSlot
|
||||||
const modelName = getItemModelName(slot, { 'minecraft:display_context': 'gui', }, appViewer.resourcesManager)
|
const modelName = getItemModelName(slot, { 'minecraft:display_context': 'gui', }, appViewer.resourcesManager, appViewer.playerState.reactive)
|
||||||
const slotCustomProps = renderSlot({ modelName, originalItemName: slot.name }, debugIsQuickbar)
|
const slotCustomProps = renderSlot({ modelName, originalItemName: slot.name }, appViewer.resourcesManager, debugIsQuickbar)
|
||||||
const itemCustomName = getItemName(slot)
|
const itemCustomName = getItemName(slot)
|
||||||
Object.assign(slot, { ...slotCustomProps, displayName: itemCustomName ?? slot.displayName })
|
Object.assign(slot, { ...slotCustomProps, displayName: itemCustomName ?? slot.displayName })
|
||||||
//@ts-expect-error
|
//@ts-expect-error
|
||||||
|
|
@ -300,12 +285,13 @@ const mapSlots = (slots: Array<RenderItem | Item | null>, isJei = false) => {
|
||||||
const { icon, ...rest } = slot
|
const { icon, ...rest } = slot
|
||||||
return rest
|
return rest
|
||||||
}
|
}
|
||||||
|
validateSlot(slot, i)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
inGameError(err)
|
inGameError(err)
|
||||||
}
|
}
|
||||||
return slot
|
return slot
|
||||||
})
|
})
|
||||||
lastMappedSlots = newSlots
|
lastMappedSlots = JSON.parse(JSON.stringify(newSlots))
|
||||||
return newSlots
|
return newSlots
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -314,6 +300,7 @@ export const upInventoryItems = (isInventory: boolean, invWindow = lastWindow) =
|
||||||
// inv.pwindow.inv.slots[2].blockData = getBlockData('dirt')
|
// inv.pwindow.inv.slots[2].blockData = getBlockData('dirt')
|
||||||
const customSlots = mapSlots((isInventory ? bot.inventory : bot.currentWindow)!.slots)
|
const customSlots = mapSlots((isInventory ? bot.inventory : bot.currentWindow)!.slots)
|
||||||
invWindow.pwindow.setSlots(customSlots)
|
invWindow.pwindow.setSlots(customSlots)
|
||||||
|
return customSlots
|
||||||
}
|
}
|
||||||
|
|
||||||
export const onModalClose = (callback: () => any) => {
|
export const onModalClose = (callback: () => any) => {
|
||||||
|
|
@ -340,6 +327,7 @@ const implementedContainersGuiMap = {
|
||||||
'minecraft:generic_3x3': 'DropDispenseWin',
|
'minecraft:generic_3x3': 'DropDispenseWin',
|
||||||
'minecraft:furnace': 'FurnaceWin',
|
'minecraft:furnace': 'FurnaceWin',
|
||||||
'minecraft:smoker': 'FurnaceWin',
|
'minecraft:smoker': 'FurnaceWin',
|
||||||
|
'minecraft:shulker_box': 'ChestWin',
|
||||||
'minecraft:blast_furnace': 'FurnaceWin',
|
'minecraft:blast_furnace': 'FurnaceWin',
|
||||||
'minecraft:crafting': 'CraftingWin',
|
'minecraft:crafting': 'CraftingWin',
|
||||||
'minecraft:crafting3x3': 'CraftingWin', // todo different result slot
|
'minecraft:crafting3x3': 'CraftingWin', // todo different result slot
|
||||||
|
|
@ -409,7 +397,7 @@ const upWindowItemsLocal = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
let skipClosePacketSending = false
|
let skipClosePacketSending = false
|
||||||
const openWindow = (type: string | undefined) => {
|
const openWindow = (type: string | undefined, title: string | any = undefined) => {
|
||||||
// if (activeModalStack.some(x => x.reactType?.includes?.('player_win:'))) {
|
// if (activeModalStack.some(x => x.reactType?.includes?.('player_win:'))) {
|
||||||
if (activeModalStack.length) { // game is not in foreground, don't close current modal
|
if (activeModalStack.length) { // game is not in foreground, don't close current modal
|
||||||
if (type) {
|
if (type) {
|
||||||
|
|
@ -430,15 +418,20 @@ const openWindow = (type: string | undefined) => {
|
||||||
lastWindow.destroy()
|
lastWindow.destroy()
|
||||||
lastWindow = null as any
|
lastWindow = null as any
|
||||||
lastWindowType = null
|
lastWindowType = null
|
||||||
window.lastWindow = lastWindow
|
window.inventory = null
|
||||||
miscUiState.displaySearchInput = false
|
miscUiState.displaySearchInput = false
|
||||||
destroyFn()
|
destroyFn()
|
||||||
skipClosePacketSending = false
|
skipClosePacketSending = false
|
||||||
|
|
||||||
|
modelViewerState.model = undefined
|
||||||
})
|
})
|
||||||
|
if (type === undefined) {
|
||||||
|
showInventoryPlayer()
|
||||||
|
}
|
||||||
cleanLoadedImagesCache()
|
cleanLoadedImagesCache()
|
||||||
const inv = openItemsCanvas(type)
|
const inv = openItemsCanvas(type)
|
||||||
inv.canvasManager.children[0].mobileHelpers = miscUiState.currentTouch
|
inv.canvasManager.children[0].mobileHelpers = miscUiState.currentTouch
|
||||||
const title = bot.currentWindow?.title
|
window.inventory = inv
|
||||||
const PrismarineChat = PrismarineChatLoader(bot.version)
|
const PrismarineChat = PrismarineChatLoader(bot.version)
|
||||||
try {
|
try {
|
||||||
inv.canvasManager.children[0].customTitleText = title ?
|
inv.canvasManager.children[0].customTitleText = title ?
|
||||||
|
|
@ -477,6 +470,7 @@ const openWindow = (type: string | undefined) => {
|
||||||
const isRightClick = type === 'rightclick'
|
const isRightClick = type === 'rightclick'
|
||||||
const isLeftClick = type === 'leftclick'
|
const isLeftClick = type === 'leftclick'
|
||||||
if (isLeftClick || isRightClick) {
|
if (isLeftClick || isRightClick) {
|
||||||
|
modelViewerState.model = undefined
|
||||||
inv.canvasManager.children[0].showRecipesOrUsages(isLeftClick, item)
|
inv.canvasManager.children[0].showRecipesOrUsages(isLeftClick, item)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -508,6 +502,7 @@ const openWindow = (type: string | undefined) => {
|
||||||
if (freeSlot === null) return
|
if (freeSlot === null) return
|
||||||
void bot.creative.setInventorySlot(freeSlot, item)
|
void bot.creative.setInventorySlot(freeSlot, item)
|
||||||
} else {
|
} else {
|
||||||
|
modelViewerState.model = undefined
|
||||||
inv.canvasManager.children[0].showRecipesOrUsages(!isRightclick, mapSlots([item], true)[0])
|
inv.canvasManager.children[0].showRecipesOrUsages(!isRightclick, mapSlots([item], true)[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -576,7 +571,7 @@ const getResultingRecipe = (slots: Array<Item | null>, gridRows: number) => {
|
||||||
type Result = RecipeItem | undefined
|
type Result = RecipeItem | undefined
|
||||||
let shapelessResult: Result
|
let shapelessResult: Result
|
||||||
let shapeResult: Result
|
let shapeResult: Result
|
||||||
outer: for (const [id, recipeVariants] of Object.entries(loadedData.recipes)) {
|
outer: for (const [id, recipeVariants] of Object.entries(loadedData.recipes ?? {})) {
|
||||||
for (const recipeVariant of recipeVariants) {
|
for (const recipeVariant of recipeVariants) {
|
||||||
if ('inShape' in recipeVariant && equals(currentShape, recipeVariant.inShape as number[][])) {
|
if ('inShape' in recipeVariant && equals(currentShape, recipeVariant.inShape as number[][])) {
|
||||||
shapeResult = recipeVariant.result!
|
shapeResult = recipeVariant.result!
|
||||||
|
|
@ -604,7 +599,7 @@ const getAllItemRecipes = (itemName: string) => {
|
||||||
const item = loadedData.itemsByName[itemName]
|
const item = loadedData.itemsByName[itemName]
|
||||||
if (!item) return
|
if (!item) return
|
||||||
const itemId = item.id
|
const itemId = item.id
|
||||||
const recipes = loadedData.recipes[itemId]
|
const recipes = loadedData.recipes?.[itemId]
|
||||||
if (!recipes) return
|
if (!recipes) return
|
||||||
const results = [] as Array<{
|
const results = [] as Array<{
|
||||||
result: Item,
|
result: Item,
|
||||||
|
|
@ -649,7 +644,7 @@ const getAllItemUsages = (itemName: string) => {
|
||||||
if (!item) return
|
if (!item) return
|
||||||
const foundRecipeIds = [] as string[]
|
const foundRecipeIds = [] as string[]
|
||||||
|
|
||||||
for (const [id, recipes] of Object.entries(loadedData.recipes)) {
|
for (const [id, recipes] of Object.entries(loadedData.recipes ?? {})) {
|
||||||
for (const recipe of recipes) {
|
for (const recipe of recipes) {
|
||||||
if ('inShape' in recipe) {
|
if ('inShape' in recipe) {
|
||||||
if (recipe.inShape.some(row => row.includes(item.id))) {
|
if (recipe.inShape.some(row => row.includes(item.id))) {
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,6 @@ export const loadSave = async (root = '/world', connectOptions?: Partial<Connect
|
||||||
}
|
}
|
||||||
|
|
||||||
let version: string | undefined | null
|
let version: string | undefined | null
|
||||||
let isFlat = false
|
|
||||||
if (levelDat) {
|
if (levelDat) {
|
||||||
version = appQueryParams.mapVersion ?? levelDat.Version?.Name
|
version = appQueryParams.mapVersion ?? levelDat.Version?.Name
|
||||||
if (!version) {
|
if (!version) {
|
||||||
|
|
@ -103,21 +102,6 @@ export const loadSave = async (root = '/world', connectOptions?: Partial<Connect
|
||||||
version = prompt(`Version ${version} is not supported, supported versions are ${supportedVersions.join(', ')}, what try to use instead?`, lowerBound ? firstSupportedVersion : lastSupportedVersion)
|
version = prompt(`Version ${version} is not supported, supported versions are ${supportedVersions.join(', ')}, what try to use instead?`, lowerBound ? firstSupportedVersion : lastSupportedVersion)
|
||||||
if (!version) return
|
if (!version) return
|
||||||
}
|
}
|
||||||
if (levelDat.WorldGenSettings) {
|
|
||||||
for (const [key, value] of Object.entries(levelDat.WorldGenSettings.dimensions)) {
|
|
||||||
if (key.slice(10) === 'overworld') {
|
|
||||||
if (value.generator.type === 'flat') isFlat = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (levelDat.generatorName) {
|
|
||||||
isFlat = levelDat.generatorName === 'flat'
|
|
||||||
}
|
|
||||||
if (!isFlat && levelDat.generatorName !== 'default' && levelDat.generatorName !== 'customized') {
|
|
||||||
// warnings.push(`Generator ${levelDat.generatorName} may not be supported yet, be careful of new chunks writes`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const playerUuid = nameToMcOfflineUUID(options.localUsername)
|
const playerUuid = nameToMcOfflineUUID(options.localUsername)
|
||||||
const playerDatPath = `${root}/playerdata/${playerUuid}.dat`
|
const playerDatPath = `${root}/playerdata/${playerUuid}.dat`
|
||||||
|
|
@ -188,11 +172,6 @@ export const loadSave = async (root = '/world', connectOptions?: Partial<Connect
|
||||||
// todo check gamemode level.dat data etc
|
// todo check gamemode level.dat data etc
|
||||||
detail: {
|
detail: {
|
||||||
version,
|
version,
|
||||||
...isFlat ? {
|
|
||||||
generation: {
|
|
||||||
name: 'superflat'
|
|
||||||
}
|
|
||||||
} : {},
|
|
||||||
...root === '/world' ? {} : {
|
...root === '/world' ? {} : {
|
||||||
'worldFolder': root
|
'worldFolder': root
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ export default async ({ tokenCaches, proxyBaseUrl, setProgressText = (text) => {
|
||||||
onMsaCodeCallback(json)
|
onMsaCodeCallback(json)
|
||||||
// this.codeCallback(json)
|
// this.codeCallback(json)
|
||||||
}
|
}
|
||||||
if (json.error) throw new Error(json.error)
|
if (json.error) throw new Error(`Auth server error: ${json.error}`)
|
||||||
if (json.token) result = json
|
if (json.token) result = json
|
||||||
if (json.newCache) setCacheResult(json.newCache)
|
if (json.newCache) setCacheResult(json.newCache)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue