Compare commits
3 commits
next
...
any-server
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e51e0e926a | ||
|
|
1ee52f7faa | ||
|
|
a7bf484880 |
345 changed files with 11865 additions and 25974 deletions
|
|
@ -1,18 +0,0 @@
|
|||
---
|
||||
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,7 +23,6 @@
|
|||
// ],
|
||||
"@stylistic/arrow-spacing": "error",
|
||||
"@stylistic/block-spacing": "error",
|
||||
"@typescript-eslint/no-this-alias": "off",
|
||||
"@stylistic/brace-style": [
|
||||
"error",
|
||||
"1tbs",
|
||||
|
|
@ -103,7 +102,6 @@
|
|||
// "@stylistic/multiline-ternary": "error", // not needed
|
||||
// "@stylistic/newline-per-chained-call": "error", // not sure if needed
|
||||
"@stylistic/new-parens": "error",
|
||||
"@typescript-eslint/class-literal-property-style": "off",
|
||||
"@stylistic/no-confusing-arrow": "error",
|
||||
"@stylistic/wrap-iife": "error",
|
||||
"@stylistic/space-before-blocks": "error",
|
||||
|
|
|
|||
59
.github/workflows/benchmark.yml
vendored
59
.github/workflows/benchmark.yml
vendored
|
|
@ -1,59 +0,0 @@
|
|||
name: Benchmark
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
push:
|
||||
branches:
|
||||
- perf-test
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
(github.event_name == 'push' && github.ref == 'refs/heads/perf-test') ||
|
||||
(
|
||||
github.event_name == 'issue_comment' &&
|
||||
github.event.issue.pull_request != '' &&
|
||||
(startsWith(github.event.comment.body, '/benchmark'))
|
||||
)
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- run: lscpu
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "pnpm"
|
||||
- name: Move Cypress to dependencies
|
||||
run: |
|
||||
jq '.dependencies.cypress = .optionalDependencies.cypress | del(.optionalDependencies.cypress)' package.json > package.json.tmp
|
||||
mv package.json.tmp package.json
|
||||
- run: pnpm install --no-frozen-lockfile
|
||||
|
||||
- run: pnpm build
|
||||
- run: nohup pnpm prod-start &
|
||||
|
||||
- run: pnpm test:benchmark
|
||||
id: benchmark
|
||||
continue-on-error: true
|
||||
# read benchmark results from stdout
|
||||
- run: |
|
||||
if [ -f benchmark.txt ]; then
|
||||
# Format the benchmark results for GitHub comment
|
||||
BENCHMARK_RESULT=$(cat benchmark.txt | sed 's/^/- /')
|
||||
echo "BENCHMARK_RESULT<<EOF" >> $GITHUB_ENV
|
||||
echo "$BENCHMARK_RESULT" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
else
|
||||
echo "BENCHMARK_RESULT=Benchmark failed to run or produce results" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- uses: mshick/add-pr-comment@v2
|
||||
with:
|
||||
allow-repeats: true
|
||||
message: |
|
||||
Benchmark result: ${{ env.BENCHMARK_RESULT }}
|
||||
2
.github/workflows/build-single-file.yml
vendored
2
.github/workflows/build-single-file.yml
vendored
|
|
@ -23,8 +23,6 @@ jobs:
|
|||
|
||||
- name: Build single-file version - 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
|
||||
uses: actions/upload-artifact@v4
|
||||
|
|
|
|||
2
.github/workflows/build-zip.yml
vendored
2
.github/workflows/build-zip.yml
vendored
|
|
@ -23,8 +23,6 @@ jobs:
|
|||
|
||||
- name: Build project
|
||||
run: pnpm build
|
||||
env:
|
||||
LOCAL_CONFIG_FILE: config.mcraft-only.json
|
||||
|
||||
- name: Bundle server.js
|
||||
run: |
|
||||
|
|
|
|||
115
.github/workflows/ci.yml
vendored
115
.github/workflows/ci.yml
vendored
|
|
@ -20,56 +20,11 @@ jobs:
|
|||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- run: pnpm install
|
||||
- run: pnpm build-single-file
|
||||
- name: Store minecraft.html size
|
||||
run: |
|
||||
SIZE_BYTES=$(du -s dist/single/minecraft.html 2>/dev/null | cut -f1)
|
||||
echo "SIZE_BYTES=$SIZE_BYTES" >> $GITHUB_ENV
|
||||
- run: pnpm check-build
|
||||
- name: Create zip package for size comparison
|
||||
run: |
|
||||
mkdir -p package
|
||||
cp -r dist package/
|
||||
cd package
|
||||
zip -r ../self-host.zip .
|
||||
- run: pnpm build-playground
|
||||
# - run: pnpm build-storybook
|
||||
- run: pnpm build-storybook
|
||||
- run: pnpm test-unit
|
||||
- run: pnpm lint
|
||||
|
||||
- name: Parse Bundle Stats
|
||||
run: |
|
||||
GZIP_BYTES=$(du -s self-host.zip 2>/dev/null | cut -f1)
|
||||
SIZE=$(echo "scale=2; $SIZE_BYTES/1024/1024" | bc)
|
||||
GZIP_SIZE=$(echo "scale=2; $GZIP_BYTES/1024/1024" | bc)
|
||||
echo "{\"total\": ${SIZE}, \"gzipped\": ${GZIP_SIZE}}" > /tmp/bundle-stats.json
|
||||
|
||||
# - name: Compare Bundle Stats
|
||||
# id: compare
|
||||
# uses: actions/github-script@v6
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GIST_TOKEN }}
|
||||
# with:
|
||||
# script: |
|
||||
# const gistId = '${{ secrets.BUNDLE_STATS_GIST_ID }}';
|
||||
|
||||
# async function getGistContent() {
|
||||
# const { data } = await github.rest.gists.get({
|
||||
# gist_id: gistId,
|
||||
# headers: {
|
||||
# authorization: `token ${process.env.GITHUB_TOKEN}`
|
||||
# }
|
||||
# });
|
||||
# return JSON.parse(data.files['bundle-stats.json'].content || '{}');
|
||||
# }
|
||||
|
||||
# const content = await getGistContent();
|
||||
# const baseStats = content['${{ github.event.pull_request.base.ref }}'];
|
||||
# const newStats = require('/tmp/bundle-stats.json');
|
||||
|
||||
# const comparison = `minecraft.html (normal build gzip)\n${baseStats.total}MB (${baseStats.gzipped}MB compressed) -> ${newStats.total}MB (${newStats.gzipped}MB compressed)`;
|
||||
# core.setOutput('stats', comparison);
|
||||
|
||||
# - run: pnpm tsx scripts/buildNpmReact.ts
|
||||
- run: nohup pnpm prod-start &
|
||||
- run: nohup pnpm test-mc-server &
|
||||
|
|
@ -85,74 +40,6 @@ jobs:
|
|||
# if: ${{ github.event.pull_request.base.ref == 'release' }}
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# - name: Store Bundle Stats
|
||||
# if: github.event.pull_request.base.ref == 'next'
|
||||
# uses: actions/github-script@v6
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GIST_TOKEN }}
|
||||
# with:
|
||||
# script: |
|
||||
# const gistId = '${{ secrets.BUNDLE_STATS_GIST_ID }}';
|
||||
|
||||
# async function getGistContent() {
|
||||
# const { data } = await github.rest.gists.get({
|
||||
# gist_id: gistId,
|
||||
# headers: {
|
||||
# authorization: `token ${process.env.GITHUB_TOKEN}`
|
||||
# }
|
||||
# });
|
||||
# return JSON.parse(data.files['bundle-stats.json'].content || '{}');
|
||||
# }
|
||||
|
||||
# async function updateGistContent(content) {
|
||||
# await github.rest.gists.update({
|
||||
# gist_id: gistId,
|
||||
# headers: {
|
||||
# authorization: `token ${process.env.GITHUB_TOKEN}`
|
||||
# },
|
||||
# files: {
|
||||
# 'bundle-stats.json': {
|
||||
# content: JSON.stringify(content, null, 2)
|
||||
# }
|
||||
# }
|
||||
# });
|
||||
# }
|
||||
|
||||
# const stats = require('/tmp/bundle-stats.json');
|
||||
# const content = await getGistContent();
|
||||
# content['${{ github.event.pull_request.base.ref }}'] = stats;
|
||||
# await updateGistContent(content);
|
||||
|
||||
# - name: Update PR Description
|
||||
# uses: actions/github-script@v6
|
||||
# with:
|
||||
# script: |
|
||||
# const { data: pr } = await github.rest.pulls.get({
|
||||
# owner: context.repo.owner,
|
||||
# repo: context.repo.repo,
|
||||
# pull_number: context.issue.number
|
||||
# });
|
||||
|
||||
# let body = pr.body || '';
|
||||
# const statsMarker = '### Bundle Size';
|
||||
# const comparison = '${{ steps.compare.outputs.stats }}';
|
||||
|
||||
# if (body.includes(statsMarker)) {
|
||||
# body = body.replace(
|
||||
# new RegExp(`${statsMarker}[^\n]*\n[^\n]*`),
|
||||
# `${statsMarker}\n${comparison}`
|
||||
# );
|
||||
# } else {
|
||||
# body += `\n\n${statsMarker}\n${comparison}`;
|
||||
# }
|
||||
|
||||
# await github.rest.pulls.update({
|
||||
# owner: context.repo.owner,
|
||||
# repo: context.repo.repo,
|
||||
# pull_number: context.issue.number,
|
||||
# body
|
||||
# });
|
||||
# dedupe-check:
|
||||
# runs-on: ubuntu-latest
|
||||
# if: github.event.pull_request.head.ref == 'next'
|
||||
|
|
|
|||
6
.github/workflows/next-deploy.yml
vendored
6
.github/workflows/next-deploy.yml
vendored
|
|
@ -30,18 +30,18 @@ jobs:
|
|||
- name: Write Release Info
|
||||
run: |
|
||||
echo "{\"latestTag\": \"$(git rev-parse --short $GITHUB_SHA)\", \"isCommit\": true}" > assets/release.json
|
||||
- name: Download Generated Sounds map
|
||||
run: node scripts/downloadSoundsMap.mjs
|
||||
- name: Build Project Artifacts
|
||||
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
|
||||
env:
|
||||
CONFIG_JSON_SOURCE: BUNDLED
|
||||
LOCAL_CONFIG_FILE: config.mcraft-only.json
|
||||
- run: pnpm build-storybook
|
||||
- name: Copy playground files
|
||||
run: |
|
||||
mkdir -p .vercel/output/static/playground
|
||||
pnpm build-playground
|
||||
cp -r renderer/dist/* .vercel/output/static/playground/
|
||||
- name: Download Generated Sounds map
|
||||
run: node scripts/downloadSoundsMap.mjs
|
||||
- name: Deploy Project Artifacts to Vercel
|
||||
uses: mathiasvr/command-output@v2.0.0
|
||||
with:
|
||||
|
|
|
|||
21
.github/workflows/preview.yml
vendored
21
.github/workflows/preview.yml
vendored
|
|
@ -1,4 +1,4 @@
|
|||
name: Vercel PR Deploy (Preview)
|
||||
name: Vercel Deploy Preview
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
|
|
@ -52,19 +52,6 @@ jobs:
|
|||
with:
|
||||
node-version: 22
|
||||
cache: "pnpm"
|
||||
- name: Update deployAlwaysUpdate packages
|
||||
run: |
|
||||
if [ -f package.json ]; then
|
||||
PACKAGES=$(node -e "const pkg = require('./package.json'); if (pkg.deployAlwaysUpdate) console.log(pkg.deployAlwaysUpdate.join(' '))")
|
||||
if [ ! -z "$PACKAGES" ]; then
|
||||
echo "Updating packages: $PACKAGES"
|
||||
pnpm up -L $PACKAGES
|
||||
else
|
||||
echo "No deployAlwaysUpdate packages found in package.json"
|
||||
fi
|
||||
else
|
||||
echo "package.json not found"
|
||||
fi
|
||||
- name: Install Global Dependencies
|
||||
run: pnpm add -g vercel
|
||||
- name: Pull Vercel Environment Information
|
||||
|
|
@ -72,13 +59,11 @@ jobs:
|
|||
- name: Write Release Info
|
||||
run: |
|
||||
echo "{\"latestTag\": \"$(git rev-parse --short ${{ github.event.pull_request.head.sha }})\", \"isCommit\": true}" > assets/release.json
|
||||
- name: Download Generated Sounds map
|
||||
run: node scripts/downloadSoundsMap.mjs
|
||||
- name: Build Project Artifacts
|
||||
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
|
||||
env:
|
||||
CONFIG_JSON_SOURCE: BUNDLED
|
||||
LOCAL_CONFIG_FILE: config.mcraft-only.json
|
||||
- run: pnpm build-storybook
|
||||
- name: Copy playground files
|
||||
run: |
|
||||
mkdir -p .vercel/output/static/playground
|
||||
|
|
@ -92,6 +77,8 @@ jobs:
|
|||
run: |
|
||||
mkdir -p .vercel/output/static/commit
|
||||
echo "<meta http-equiv='refresh' content='0;url=https://github.com/${{ github.repository }}/pull/${{ github.event.issue.number || github.event.pull_request.number }}/commits/${{ github.event.pull_request.head.sha }}'>" > .vercel/output/static/commit/index.html
|
||||
- name: Download Generated Sounds map
|
||||
run: node scripts/downloadSoundsMap.mjs
|
||||
- name: Deploy Project Artifacts to Vercel
|
||||
uses: mathiasvr/command-output@v2.0.0
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -29,18 +29,22 @@ jobs:
|
|||
run: pnpx zardoy-release empty --skip-github --output-file assets/release.json
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Download Generated Sounds map
|
||||
run: node scripts/downloadSoundsMap.mjs
|
||||
- run: vercel build --token=${{ secrets.VERCEL_TOKEN }} --prod
|
||||
env:
|
||||
CONFIG_JSON_SOURCE: BUNDLED
|
||||
LOCAL_CONFIG_FILE: config.mcraft-only.json
|
||||
- run: pnpm build-storybook
|
||||
- name: Copy playground files
|
||||
run: |
|
||||
mkdir -p .vercel/output/static/playground
|
||||
pnpm build-playground
|
||||
cp -r renderer/dist/* .vercel/output/static/playground/
|
||||
|
||||
- name: Download Generated Sounds map
|
||||
run: node scripts/downloadSoundsMap.mjs
|
||||
- name: Deploy Project to Vercel
|
||||
uses: mathiasvr/command-output@v2.0.0
|
||||
with:
|
||||
run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} --prod
|
||||
id: deploy
|
||||
# publish to github
|
||||
- run: cp vercel.json .vercel/output/static/vercel.json
|
||||
- uses: peaceiris/actions-gh-pages@v3
|
||||
|
|
@ -49,39 +53,6 @@ jobs:
|
|||
publish_dir: .vercel/output/static
|
||||
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
|
||||
run: |
|
||||
# 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
|
||||
|
||||
- name: Deploy Project to Vercel
|
||||
uses: mathiasvr/command-output@v2.0.0
|
||||
with:
|
||||
run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} --prod
|
||||
id: deploy
|
||||
- name: Get releasing alias
|
||||
run: node scripts/githubActions.mjs getReleasingAlias
|
||||
id: alias
|
||||
- name: Set deployment alias
|
||||
run: |
|
||||
for alias in $(echo ${{ steps.alias.outputs.alias }} | tr "," "\n"); do
|
||||
vercel alias set ${{ steps.deploy.outputs.stdout }} $alias --token=${{ secrets.VERCEL_TOKEN }} --scope=zaro
|
||||
done
|
||||
|
||||
- name: Build single-file version - minecraft.html
|
||||
run: pnpm build-single-file && mv dist/single/index.html minecraft.html
|
||||
- name: Build self-host version
|
||||
|
|
@ -99,7 +70,7 @@ jobs:
|
|||
zip -r ../self-host.zip .
|
||||
|
||||
- run: |
|
||||
pnpx zardoy-release node --footer "This release URL: https://$(echo ${{ steps.alias.outputs.alias }} | cut -d',' -f1) (Vercel URL: ${{ steps.deploy.outputs.stdout }})"
|
||||
pnpx zardoy-release node --footer "This release URL: ${{ steps.deploy.outputs.stdout }}"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# has possible output: tag
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -10,7 +10,7 @@ localSettings.mjs
|
|||
dist*
|
||||
.DS_Store
|
||||
.idea/
|
||||
/world
|
||||
world
|
||||
data*.json
|
||||
out
|
||||
*.iml
|
||||
|
|
@ -19,6 +19,5 @@ generated
|
|||
storybook-static
|
||||
server-jar
|
||||
config.local.json
|
||||
logs/
|
||||
|
||||
src/react/npmReactComponents.ts
|
||||
|
|
|
|||
|
|
@ -9,10 +9,8 @@ After forking the repository, run the following commands to get started:
|
|||
4. Let us know if you are working on something and be sure to open a PR if you got any changes. Happy coding!
|
||||
|
||||
*(1): If you are getting `Cannot find matching keyid` update corepack to the latest version with `npm i -g corepack`.
|
||||
|
||||
*(2): If still something doesn't work ensure you have the right nodejs version with `node -v` (tested on 22.x)
|
||||
|
||||
<!-- *(3): For GitHub codespaces (cloud ide): Run `pnpm i @rsbuild/core@1.2.4 @rsbuild/plugin-node-polyfill@1.3.0 @rsbuild/plugin-react@1.1.0 @rsbuild/plugin-typed-css-modules@1.0.2` command to avoid crashes because of limited ram -->
|
||||
*(3): For GitHub codespaces (cloud ide): Run `pnpm i @rsbuild/core@1.2.4 @rsbuild/plugin-node-polyfill@1.3.0 @rsbuild/plugin-react@1.1.0 @rsbuild/plugin-typed-css-modules@1.0.2` command to avoid crashes because of limited ram
|
||||
|
||||
## Project Structure
|
||||
|
||||
|
|
@ -177,13 +175,8 @@ New React components, improve UI (including mobile support).
|
|||
|
||||
## Updating Dependencies
|
||||
|
||||
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`
|
||||
|
||||
1. Ensure mineflayer fork is up to date with the latest version of mineflayer original repo
|
||||
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:
|
||||
1. Remove the patch from `patchedDependencies` in `package.json`
|
||||
2. Run `pnpm patch minecraft-protocol`, open patch directory
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ FROM node:18-alpine AS build
|
|||
RUN apk add git
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
# install pnpm with corepack
|
||||
RUN corepack enable
|
||||
# install pnpm
|
||||
RUN npm i -g pnpm@9.0.4
|
||||
# Build arguments
|
||||
ARG DOWNLOAD_SOUNDS=false
|
||||
ARG DISABLE_SERVICE_WORKER=false
|
||||
|
|
@ -35,7 +35,7 @@ WORKDIR /app
|
|||
COPY --from=build /app/dist /app/dist
|
||||
COPY server.js /app/server.js
|
||||
# Install express
|
||||
RUN npm i -g pnpm@10.8.0
|
||||
RUN npm i -g pnpm@9.0.4
|
||||
RUN npm init -yp
|
||||
RUN pnpm i express github:zardoy/prismarinejs-net-browserify compression cors
|
||||
EXPOSE 8080
|
||||
|
|
|
|||
40
README.MD
40
README.MD
|
|
@ -6,17 +6,12 @@ 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.
|
||||
|
||||
> For Turkey/Russia use [ru.mcraft.fun](https://ru.mcraft.fun/) (since Cloudflare is blocked)
|
||||
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!
|
||||
|
||||
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)
|
||||
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).
|
||||
|
||||
### Big Features
|
||||
|
||||
- Official Mineflayer [plugin integration](https://github.com/zardoy/mcraft-fun-mineflayer-plugin)! View / Control your bot remotely.
|
||||
- Open any zip world file or even folder in read-write mode!
|
||||
- Connect to Java servers running in both offline (cracked) and online mode* (it's possible because of proxy servers, see below)
|
||||
- Integrated JS server clone capable of opening Java world saves in any way (folders, zip, web chunks streaming, etc)
|
||||
|
|
@ -29,39 +24,24 @@ For building the project yourself / contributing, see [Development, Debugging &
|
|||
- Custom protocol channel extensions (eg for custom block models in the world)
|
||||
- Play with friends over internet! (P2P is powered by Peer.js discovery servers)
|
||||
- ~~Google Drive support for reading / saving worlds back to the cloud~~
|
||||
- Support for custom rendering 3D engines. Modular architecture.
|
||||
- even even more!
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
### Recommended Settings
|
||||
|
||||
- Controls -> **Touch Controls Type** -> **Joystick**
|
||||
- Controls -> **Auto Full Screen** -> **On** - To avoid ctrl+w issue
|
||||
- Interface -> **Enable Minimap** -> **Always** - To enable useful minimap (why not?)
|
||||
- Controls -> **Raw Input** -> **On** - This will make the controls more precise (UPD: already enabled by default)
|
||||
- Interface -> **Chat Select** -> **On** - To select chat messages (UPD: already enabled by default)
|
||||
|
||||
### Browser Notes
|
||||
|
||||
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:
|
||||
These browsers have issues with capturing pointer:
|
||||
|
||||
**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
|
||||
|
||||
### Versions Support
|
||||
|
||||
Server versions 1.8 - 1.21.5 are supported.
|
||||
First class versions (most of the features are tested on these versions):
|
||||
|
||||
- 1.19.4
|
||||
- 1.21.4
|
||||
|
||||
Versions below 1.13 are not tested currently and may not work correctly.
|
||||
|
||||
### World Loading
|
||||
|
||||
Zip files and folders are supported. Just drag and drop them into the browser window. You can open folders in readonly and read-write mode. New chunks may be generated incorrectly for now.
|
||||
|
|
@ -78,8 +58,6 @@ 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)
|
||||
|
||||
> **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.
|
||||
|
||||
```mermaid
|
||||
|
|
@ -127,12 +105,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:
|
||||
|
||||
- If you type `debugToggle`, press enter in console - It will enables all debug messages! Warning: this will start all packets spam.
|
||||
- `localStorage.debug = '*'` - 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
|
||||
|
||||
- `bot` - Mineflayer bot instance. See Mineflayer documentation for more.
|
||||
- `world` - Three.js world instance, basically does all the rendering (part of renderer backend).
|
||||
- `world.sectionObjects` - Object with all active chunk sections (geometries) in the world. Each chunk section is a Three.js mesh or group.
|
||||
- `viewer` - Three.js viewer instance, basically does all the rendering.
|
||||
- `viewer.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).
|
||||
- `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.
|
||||
|
|
@ -141,7 +119,7 @@ Instead I recommend setting `options.debugLogNotFrequentPackets`. Also you can u
|
|||
|
||||
- `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 `world.getCameraPosition()` 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 `viewer.camera.position` to see the camera position and so on.
|
||||
|
||||
<img src="./docs-assets/watch-expr.png" alt="Watch expression" width="480"/>
|
||||
|
||||
|
|
@ -178,7 +156,6 @@ 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.
|
||||
- `?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.
|
||||
- `?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:
|
||||
|
||||
|
|
@ -235,4 +212,3 @@ Only during development:
|
|||
|
||||
- [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)
|
||||
- [js-minecraft](https://github.com/LabyStudio/js-minecraft) - An insanely well done clone from the graphical side that inspired many features here
|
||||
|
|
|
|||
13
TECH.md
13
TECH.md
|
|
@ -10,27 +10,26 @@ This client generally has better performance but some features reproduction migh
|
|||
| Gamepad Support | ✅ | ❌ | |
|
||||
| A11Y | ✅ | ❌ | We have DOM for almost all UI so your extensions and other browser features will work natively like on any other web page (but maybe it's not needed) |
|
||||
| Game Features | | | |
|
||||
| Servers Support (quality) | ❌(+) | ✅ | Eaglercraft is vanilla Minecraft, while this project tries to emulate original game behavior at protocol level (Mineflayer is used) |
|
||||
| Servers Support (quality) | ❌ | ✅ | Eaglercraft is vanilla Minecraft, while this project tries to emulate original game behavior at protocol level (Mineflayer is used) |
|
||||
| Servers Support (any version, ip) | ✅ | ❌ | We support almost all Minecraft versions, only important if you connect to a server where you need new content like blocks or if you play with friends. And you can connect to almost any server using proxy servers! |
|
||||
| Servers Support (online mode) | ✅ | ❌ | Join to online servers like Hypixel using your Microsoft account without additional proxies |
|
||||
| Singleplayer Survival Features | ❌ | ✅ | Just like Eaglercraft this project can generate and save worlds, but generator is simple and only a few survival features are supported (look here for [supported features list](https://github.com/zardoy/space-squid)) |
|
||||
| Singleplayer Maps | ✅ | ✅ | We support any version, but adventure maps won't work, but simple parkour and build maps might be interesting to explore... |
|
||||
| Singleplayer Maps World Streaming | ✅ | ❌ | Thanks to Browserfs, saves can be loaded to local singleplayer server using multiple ways: from local folder, server directory (not zip), dropbox or other cloud *backend* etc... |
|
||||
| P2P Multiplayer | ✅ | ✅ | A way to connect to other browser running the project. But it's almost useless here since many survival features are not implemented. Maybe only to build / explore maps together... |
|
||||
| Voice Chat | ❌(+) | ✅ | Eaglercraft has custom WebRTC voice chat implementation, though it could also be easily implemented there |
|
||||
| Voice Chat | ❌ | ✅ | Eaglercraft has custom WebRTC voice chat implementation, though it could also be easily implemented there |
|
||||
| Online Servers | ✅ | ❌ | We have custom implementation (including integration on proxy side) for joining to servers |
|
||||
| Plugin Features | ✅ | ❌ | We have Mineflayer plugins support, like Auto Jump & Auto Parkour was added here that way |
|
||||
| Direct Connection | ✅ | ✅ | We have DOM for almost all UI so your extensions and other browser features will work natively like on any other web page |
|
||||
| Moding | ✅(own js mods) | ❌ | This project will support mods for singleplayer. In theory its possible to implement support for modded servers on protocol level (including all needed mods) |
|
||||
| Video Recording | ❌ | ✅ | Doesn't feel needed |
|
||||
| Metaverse Features | ✅(50%) | ❌ | We have videos / images support inside world, but not iframes (custom protocol channel) |
|
||||
| Moding | ❌(roadmap, client-side) | ❌ | This project will support mods for singleplayer. In theory its possible to implement support for modded servers on protocol level (including all needed mods) |
|
||||
| Video Recording | ❌ | ✅ | Don't feel needed |
|
||||
| Metaverse Features | ❌(roadmap) | ❌ | Iframes, video streams inside of game world (custom protocol channel) |
|
||||
| Sounds | ✅ | ✅ | |
|
||||
| Resource Packs | ✅(+extras) | ✅ | This project has very limited support for them (only textures images are loadable for now) |
|
||||
| Assets Compressing & Splitting | ✅ | ❌ | We have advanced Minecraft data processing and good code chunk splitting so the web app will open much faster and use less memory |
|
||||
| Graphics | | | |
|
||||
| Fancy Graphics | ❌ | ✅ | While Eaglercraft has top-level shaders we don't even support lighting |
|
||||
| Fast & Efficient Graphics | ❌(+) | ❌ | Feels like no one needs to have 64 rendering distance work smoothly |
|
||||
| VR | ✅(-) | ❌ | Feels like not needed feature. UI is missing in this project since DOM can't be rendered in VR so Eaglercraft could be better in that aspect |
|
||||
| VR | ✅ | ❌ | Feels like not needed feature. UI is missing in this project since DOM can't be rendered in VR so Eaglercraft could be better in that aspect |
|
||||
| AR | ❌ | ❌ | Would be the most useless feature |
|
||||
| Minimap & Waypoints | ✅(-) | ❌ | We have buggy minimap, which can be enabled in settings and full map is opened by pressing `M` key |
|
||||
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Configure client</title>
|
||||
<script>
|
||||
function removeSettings() {
|
||||
if (confirm('Are you sure you want to RESET ALL SETTINGS?')) {
|
||||
localStorage.setItem('options', '{}');
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
function removeAllData() {
|
||||
localStorage.removeItem('serversList')
|
||||
localStorage.removeItem('serversHistory')
|
||||
localStorage.removeItem('authenticatedAccounts')
|
||||
localStorage.removeItem('modsAutoUpdateLastCheck')
|
||||
localStorage.removeItem('firstModsPageVisit')
|
||||
localStorage.removeItem('proxiesData')
|
||||
localStorage.removeItem('keybindings')
|
||||
localStorage.removeItem('username')
|
||||
localStorage.removeItem('customCommands')
|
||||
localStorage.removeItem('options')
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div style="display: flex;gap: 10px;">
|
||||
<button onclick="removeSettings()">Reset all settings</button>
|
||||
<button onclick="removeAllData()">Remove all user data (but not mods or worlds)</button>
|
||||
<!-- <button>Remove all user data (worlds, resourcepacks)</button> -->
|
||||
<!-- <button>Remove all mods</button> -->
|
||||
<!-- <button>Remove all mod repositories</button> -->
|
||||
</div>
|
||||
<input />
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
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/
|
||||
|
|
@ -1,237 +0,0 @@
|
|||
<!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>
|
||||
53
config.json
53
config.json
|
|
@ -3,36 +3,25 @@
|
|||
"defaultHost": "<from-proxy>",
|
||||
"defaultProxy": "https://proxy.mcraft.fun",
|
||||
"mapsProvider": "https://maps.mcraft.fun/",
|
||||
"skinTexturesProxy": "",
|
||||
"peerJsServer": "",
|
||||
"peerJsServerFallback": "https://p2p.mcraft.fun",
|
||||
"promoteServers": [
|
||||
{
|
||||
"ip": "wss://mcraft.ryzyn.xyz",
|
||||
"version": "1.19.4"
|
||||
},
|
||||
{
|
||||
"ip": "wss://play.mcraft.fun"
|
||||
},
|
||||
{
|
||||
"ip": "wss://play.webmc.fun",
|
||||
"name": "WebMC"
|
||||
},
|
||||
{
|
||||
"ip": "wss://ws.fuchsmc.net"
|
||||
},
|
||||
{
|
||||
"ip": "wss://play2.mcraft.fun"
|
||||
},
|
||||
{
|
||||
"ip": "wss://play-creative.mcraft.fun",
|
||||
"description": "Might be available soon, stay tuned!"
|
||||
},
|
||||
{
|
||||
"ip": "kaboom.pw",
|
||||
"version": "1.20.3",
|
||||
"description": "Very nice a polite server. Must try for everyone!"
|
||||
}
|
||||
],
|
||||
"rightSideText": "A Minecraft client clone in the browser!",
|
||||
"splashText": "The sunset is coming!",
|
||||
"splashTextFallback": "Welcome!",
|
||||
"pauseLinks": [
|
||||
[
|
||||
{
|
||||
|
|
@ -42,39 +31,5 @@
|
|||
"type": "discord"
|
||||
}
|
||||
]
|
||||
],
|
||||
"defaultUsername": "mcrafter{0-9999}",
|
||||
"mobileButtons": [
|
||||
{
|
||||
"action": "general.drop",
|
||||
"actionHold": "general.dropStack",
|
||||
"label": "Q"
|
||||
},
|
||||
{
|
||||
"action": "general.selectItem",
|
||||
"actionHold": "",
|
||||
"label": "S"
|
||||
},
|
||||
{
|
||||
"action": "general.debugOverlay",
|
||||
"actionHold": "general.debugOverlayHelpMenu",
|
||||
"label": "F3"
|
||||
},
|
||||
{
|
||||
"action": "general.playersList",
|
||||
"actionHold": "",
|
||||
"icon": "pixelarticons:users",
|
||||
"label": "TAB"
|
||||
},
|
||||
{
|
||||
"action": "general.chat",
|
||||
"actionHold": "",
|
||||
"label": ""
|
||||
},
|
||||
{
|
||||
"action": "ui.pauseMenu",
|
||||
"actionHold": "",
|
||||
"label": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"alwaysReconnectButton": true,
|
||||
"reportBugButtonWithReconnect": true,
|
||||
"allowAutoConnect": true
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
import { defineConfig } from 'cypress'
|
||||
|
||||
const isPerformanceTest = process.env.PERFORMANCE_TEST === 'true'
|
||||
|
||||
export default defineConfig({
|
||||
video: false,
|
||||
chromeWebSecurity: false,
|
||||
|
|
@ -34,7 +32,7 @@ export default defineConfig({
|
|||
return require('./cypress/plugins/index.js')(on, config)
|
||||
},
|
||||
baseUrl: 'http://localhost:8080',
|
||||
specPattern: !isPerformanceTest ? 'cypress/e2e/smoke.spec.ts' : 'cypress/e2e/rendering_performance.spec.ts',
|
||||
specPattern: 'cypress/e2e/**/*.spec.ts',
|
||||
excludeSpecPattern: ['**/__snapshots__/*', '**/__image_snapshots__/*'],
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
/// <reference types="cypress" />
|
||||
import { BenchmarkAdapterInfo, getAllInfoLines } from '../../src/benchmarkAdapter'
|
||||
import { cleanVisit } from './shared'
|
||||
|
||||
it('Benchmark rendering performance', () => {
|
||||
cleanVisit('/?openBenchmark=true&renderDistance=5')
|
||||
// wait for render end event
|
||||
return cy.document().then({ timeout: 180_000 }, doc => {
|
||||
return new Cypress.Promise(resolve => {
|
||||
cy.log('Waiting for world to load')
|
||||
doc.addEventListener('cypress-world-ready', resolve)
|
||||
}).then(() => {
|
||||
cy.log('World loaded')
|
||||
})
|
||||
}).then(() => {
|
||||
cy.window().then(win => {
|
||||
const adapter = win.benchmarkAdapter as BenchmarkAdapterInfo
|
||||
|
||||
const messages = getAllInfoLines(adapter)
|
||||
// wait for 10 seconds
|
||||
cy.wait(10_000)
|
||||
const messages2 = getAllInfoLines(adapter, true)
|
||||
for (const message of messages) {
|
||||
cy.log(message)
|
||||
}
|
||||
for (const message of messages2) {
|
||||
cy.log(message)
|
||||
}
|
||||
cy.writeFile('benchmark.txt', [...messages, ...messages2].join('\n'))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
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()
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
<script type="module" src="three-labels.ts"></script>
|
||||
<style>
|
||||
body { margin: 0; }
|
||||
canvas { display: block; }
|
||||
</style>
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
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,60 +1,101 @@
|
|||
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 camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
|
||||
camera.position.set(0, 0, 5)
|
||||
const renderer = new THREE.WebGLRenderer()
|
||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
document.body.appendChild(renderer.domElement)
|
||||
|
||||
// Position camera
|
||||
camera.position.z = 5
|
||||
const controls = new OrbitControls(camera, renderer.domElement)
|
||||
|
||||
// Create a canvas with some content
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = 256
|
||||
canvas.height = 256
|
||||
const ctx = canvas.getContext('2d')
|
||||
const geometry = new THREE.BoxGeometry(1, 1, 1)
|
||||
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
|
||||
const cube = new THREE.Mesh(geometry, material)
|
||||
cube.position.set(0.5, 0.5, 0.5);
|
||||
const group = new THREE.Group()
|
||||
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)
|
||||
|
||||
scene.background = new THREE.Color(0x444444)
|
||||
// const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshBasicMaterial({ color: 0x00_00_ff, transparent: true, opacity: 0.5 }))
|
||||
// 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)
|
||||
|
||||
// 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)
|
||||
new tweenJs.Tween(group.rotation).to({ z: THREE.MathUtils.degToRad(90) }, 1000).yoyo(true).repeat(Infinity).start()
|
||||
|
||||
// Create bitmap and texture
|
||||
async function createTexturedBox() {
|
||||
const canvas2 = new OffscreenCanvas(256, 256)
|
||||
const ctx2 = canvas2.getContext('2d')!
|
||||
ctx2.drawImage(canvas, 0, 0)
|
||||
const texture = new THREE.Texture(canvas2)
|
||||
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)
|
||||
const tweenGroup = new tweenJs.Group()
|
||||
function animate () {
|
||||
tweenGroup.update()
|
||||
requestAnimationFrame(animate)
|
||||
// cube.rotation.x += 0.01
|
||||
// cube.rotation.y += 0.01
|
||||
renderer.render(scene, camera)
|
||||
}
|
||||
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);
|
||||
})
|
||||
|
|
|
|||
25
index.html
25
index.html
|
|
@ -27,7 +27,6 @@
|
|||
<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 -->
|
||||
<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>
|
||||
`
|
||||
|
|
@ -37,13 +36,6 @@
|
|||
if (!window.pageLoaded) {
|
||||
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
|
||||
const onError = (errorOrMessage, log = false) => {
|
||||
let message = errorOrMessage instanceof Error ? (errorOrMessage.stack ?? errorOrMessage.message) : errorOrMessage
|
||||
|
|
@ -54,23 +46,12 @@
|
|||
const [errorMessage, ...errorStack] = message.split('\n')
|
||||
document.querySelector('.initial-loader').querySelector('.subtitle').textContent = errorMessage
|
||||
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
|
||||
// unregister all sw
|
||||
if (window.navigator.serviceWorker && document.querySelector('.initial-loader').style.opacity !== 0) {
|
||||
console.log('got worker')
|
||||
if (window.navigator.serviceWorker) {
|
||||
window.navigator.serviceWorker.getRegistrations().then(registrations => {
|
||||
registrations.forEach(registration => {
|
||||
console.log('got registration')
|
||||
registration.unregister().then(() => {
|
||||
console.log('worker unregistered')
|
||||
})
|
||||
registration.unregister()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -149,7 +130,7 @@
|
|||
</script> -->
|
||||
<title>Minecraft Web Client</title>
|
||||
<!-- <link rel="canonical" href="https://mcraft.fun"> -->
|
||||
<meta name="description" content="Minecraft Java Edition Client in Browser — Full Multiplayer Support, Server Connect, Offline Play — Join real Minecraft servers">
|
||||
<meta name="description" content="Minecraft web client running in your browser">
|
||||
<meta name="keywords" content="Play, Minecraft, Online, Web, Java, Server, Single player, Javascript, PrismarineJS, Voxel, WebGL, Three.js">
|
||||
<meta name="date" content="2024-07-11" scheme="YYYY-MM-DD">
|
||||
<meta name="language" content="English">
|
||||
|
|
|
|||
71
package.json
71
package.json
|
|
@ -7,14 +7,12 @@
|
|||
"dev-proxy": "node server.js",
|
||||
"start": "run-p dev-proxy 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-analyze": "BUNDLE_ANALYZE=true rsbuild build && pnpm build-other-workers",
|
||||
"build-single-file": "SINGLE_FILE_BUILD=true rsbuild build",
|
||||
"prepare-project": "tsx scripts/genShims.ts && tsx scripts/makeOptimizedMcData.mjs && tsx scripts/genLargeDataAliases.ts",
|
||||
"check-build": "pnpm prepare-project && tsc && pnpm build",
|
||||
"test:cypress": "cypress run",
|
||||
"test:benchmark": "PERFORMANCE_TEST=true cypress run",
|
||||
"test:cypress:open": "cypress open",
|
||||
"test-unit": "vitest",
|
||||
"test:e2e": "start-test http-get://localhost:8080 test:cypress",
|
||||
|
|
@ -32,9 +30,7 @@
|
|||
"run-playground": "run-p watch-mesher watch-other-workers watch-playground",
|
||||
"run-all": "run-p start run-playground",
|
||||
"build-playground": "rsbuild build --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"
|
||||
"watch-playground": "rsbuild dev --config renderer/rsbuild.config.ts"
|
||||
},
|
||||
"keywords": [
|
||||
"prismarine",
|
||||
|
|
@ -54,9 +50,8 @@
|
|||
"dependencies": {
|
||||
"@dimaka/interface": "0.0.3-alpha.0",
|
||||
"@floating-ui/react": "^0.26.1",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@nxg-org/mineflayer-auto-jump": "^0.7.18",
|
||||
"@nxg-org/mineflayer-tracker": "1.3.0",
|
||||
"@nxg-org/mineflayer-auto-jump": "^0.7.12",
|
||||
"@nxg-org/mineflayer-tracker": "1.2.1",
|
||||
"@react-oauth/google": "^0.12.1",
|
||||
"@stylistic/eslint-plugin": "^2.6.1",
|
||||
"@types/gapi": "^0.0.47",
|
||||
|
|
@ -80,19 +75,18 @@
|
|||
"esbuild-plugin-polyfill-node": "^0.3.0",
|
||||
"express": "^4.18.2",
|
||||
"filesize": "^10.0.12",
|
||||
"flying-squid": "npm:@zardoy/flying-squid@^0.0.104",
|
||||
"framer-motion": "^12.9.2",
|
||||
"flying-squid": "npm:@zardoy/flying-squid@^0.0.51",
|
||||
"fs-extra": "^11.1.1",
|
||||
"google-drive-browserfs": "github:zardoy/browserfs#google-drive",
|
||||
"jszip": "^3.10.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mcraft-fun-mineflayer": "^0.1.23",
|
||||
"minecraft-data": "3.98.0",
|
||||
"minecraft-data": "3.83.1",
|
||||
"minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master",
|
||||
"mineflayer-item-map-downloader": "github:zardoy/mineflayer-item-map-downloader",
|
||||
"mojangson": "^2.0.4",
|
||||
"net-browserify": "github:zardoy/prismarinejs-net-browserify",
|
||||
"node-gzip": "^1.1.2",
|
||||
"mcraft-fun-mineflayer": "^0.1.10",
|
||||
"peerjs": "^1.5.0",
|
||||
"pixelarticons": "^1.8.1",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
|
|
@ -106,6 +100,7 @@
|
|||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-select": "^5.8.0",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"react-zoom-pan-pinch": "3.4.4",
|
||||
"remark": "^15.0.1",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
|
|
@ -123,11 +118,11 @@
|
|||
"workbox-build": "^7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rsbuild/core": "1.3.5",
|
||||
"@rsbuild/plugin-node-polyfill": "1.3.0",
|
||||
"@rsbuild/plugin-react": "1.2.0",
|
||||
"@rsbuild/plugin-type-check": "1.2.1",
|
||||
"@rsbuild/plugin-typed-css-modules": "1.0.2",
|
||||
"@rsbuild/core": "^1.0.1-beta.9",
|
||||
"@rsbuild/plugin-node-polyfill": "^1.0.3",
|
||||
"@rsbuild/plugin-react": "^1.0.1-beta.9",
|
||||
"@rsbuild/plugin-type-check": "^1.0.1-beta.9",
|
||||
"@rsbuild/plugin-typed-css-modules": "^1.0.1",
|
||||
"@storybook/addon-essentials": "^7.4.6",
|
||||
"@storybook/addon-links": "^7.4.6",
|
||||
"@storybook/blocks": "^7.4.6",
|
||||
|
|
@ -135,6 +130,7 @@
|
|||
"@storybook/react-vite": "^7.4.6",
|
||||
"@types/diff-match-patch": "^1.0.36",
|
||||
"@types/lodash-es": "^4.17.9",
|
||||
"@types/react-transition-group": "^4.4.7",
|
||||
"@types/stats.js": "^0.17.1",
|
||||
"@types/three": "0.154.0",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
|
|
@ -144,7 +140,7 @@
|
|||
"browserify-zlib": "^0.2.0",
|
||||
"buffer": "^6.0.3",
|
||||
"constants-browserify": "^1.0.0",
|
||||
"contro-max": "^0.1.9",
|
||||
"contro-max": "^0.1.8",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"cypress-esbuild-preprocessor": "^1.0.2",
|
||||
"eslint": "^8.50.0",
|
||||
|
|
@ -154,16 +150,17 @@
|
|||
"http-browserify": "^1.7.0",
|
||||
"http-server": "^14.1.1",
|
||||
"https-browserify": "^1.0.0",
|
||||
"mc-assets": "^0.2.62",
|
||||
"mc-assets": "^0.2.42",
|
||||
"mineflayer-mouse": "^0.0.9",
|
||||
"minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next",
|
||||
"mineflayer": "github:zardoy/mineflayer#gen-the-master",
|
||||
"mineflayer-mouse": "^0.1.21",
|
||||
"mineflayer": "github:zardoy/mineflayer",
|
||||
"mineflayer-pathfinder": "^2.4.4",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"os-browserify": "^0.3.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"path-exists-cli": "^2.0.0",
|
||||
"process": "github:PrismarineJS/node-process",
|
||||
"renderer": "link:renderer",
|
||||
"process": "github:PrismarineJS/node-process",
|
||||
"rimraf": "^5.0.1",
|
||||
"storybook": "^7.4.6",
|
||||
"stream-browserify": "^3.0.0",
|
||||
|
|
@ -197,15 +194,14 @@
|
|||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"mineflayer": "github:zardoy/mineflayer#gen-the-master",
|
||||
"@nxg-org/mineflayer-physics-util": "1.8.10",
|
||||
"buffer": "^6.0.3",
|
||||
"vec3": "0.1.10",
|
||||
"@nxg-org/mineflayer-physics-util": "1.5.8",
|
||||
"three": "0.154.0",
|
||||
"diamond-square": "github:zardoy/diamond-square",
|
||||
"prismarine-block": "github:zardoy/prismarine-block#next-era",
|
||||
"prismarine-world": "github:zardoy/prismarine-world#next-era",
|
||||
"minecraft-data": "3.98.0",
|
||||
"minecraft-data": "3.83.1",
|
||||
"prismarine-provider-anvil": "github:zardoy/prismarine-provider-anvil#everything",
|
||||
"prismarine-physics": "github:zardoy/prismarine-physics",
|
||||
"minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master",
|
||||
|
|
@ -214,29 +210,14 @@
|
|||
"prismarine-item": "latest"
|
||||
},
|
||||
"updateConfig": {
|
||||
"ignoreDependencies": [
|
||||
"browserfs",
|
||||
"google-drive-browserfs"
|
||||
]
|
||||
"ignoreDependencies": []
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"three@0.154.0": "patches/three@0.154.0.patch",
|
||||
"pixelarticons@1.8.1": "patches/pixelarticons@1.8.1.patch",
|
||||
"mineflayer-item-map-downloader@1.2.0": "patches/mineflayer-item-map-downloader@1.2.0.patch",
|
||||
"minecraft-protocol": "patches/minecraft-protocol.patch"
|
||||
},
|
||||
"ignoredBuiltDependencies": [
|
||||
"canvas",
|
||||
"core-js",
|
||||
"gl"
|
||||
],
|
||||
"onlyBuiltDependencies": [
|
||||
"sharp",
|
||||
"cypress",
|
||||
"esbuild",
|
||||
"fsevents"
|
||||
],
|
||||
"ignorePatchFailures": false,
|
||||
"allowUnusedPatches": false
|
||||
"minecraft-protocol@1.54.0": "patches/minecraft-protocol@1.54.0.patch"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.8.0+sha512.0e82714d1b5b43c74610193cb20734897c1d00de89d0e18420aebc5977fa13d780a9cb05734624e81ebd81cc876cd464794850641c48b9544326b5622ca29971"
|
||||
"packageManager": "pnpm@9.0.4"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
diff --git a/src/client/chat.js b/src/client/chat.js
|
||||
index 0021870994fc59a82f0ac8aba0a65a8be43ef2f4..a53fceb843105ea2a1d88722b3fc7c3b43cb102a 100644
|
||||
index f14269bea055d4329cd729271e7406ec4b344de7..00f5482eb6e3c911381ca9a728b1b4aae0d1d337 100644
|
||||
--- a/src/client/chat.js
|
||||
+++ b/src/client/chat.js
|
||||
@@ -116,7 +116,7 @@ module.exports = function (client, options) {
|
||||
for (const player of packet.data) {
|
||||
if (player.chatSession) {
|
||||
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' }),
|
||||
publicKeyDER: player.chatSession.publicKey.keyBytes,
|
||||
sessionUuid: player.chatSession.uuid
|
||||
}
|
||||
@@ -126,7 +126,7 @@ module.exports = function (client, options) {
|
||||
|
||||
if (player.crypto) {
|
||||
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' }),
|
||||
publicKeyDER: player.crypto.publicKey,
|
||||
signature: player.crypto.signature,
|
||||
displayName: player.displayName || player.name
|
||||
@@ -196,7 +196,7 @@ module.exports = function (client, options) {
|
||||
@@ -111,7 +111,7 @@ module.exports = function (client, options) {
|
||||
for (const player of packet.data) {
|
||||
if (!player.chatSession) continue
|
||||
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' }),
|
||||
publicKeyDER: player.chatSession.publicKey.keyBytes,
|
||||
sessionUuid: player.chatSession.uuid
|
||||
}
|
||||
@@ -127,7 +127,7 @@ module.exports = function (client, options) {
|
||||
for (const player of packet.data) {
|
||||
if (player.crypto) {
|
||||
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' }),
|
||||
publicKeyDER: player.crypto.publicKey,
|
||||
signature: player.crypto.signature,
|
||||
displayName: player.displayName || player.name
|
||||
@@ -198,7 +198,7 @@ module.exports = function (client, options) {
|
||||
if (mcData.supportFeature('useChatSessions')) {
|
||||
const tsDelta = BigInt(Date.now()) - packet.timestamp
|
||||
const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0
|
||||
|
|
@ -28,8 +28,8 @@ index 0021870994fc59a82f0ac8aba0a65a8be43ef2f4..a53fceb843105ea2a1d88722b3fc7c3b
|
|||
+ 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)
|
||||
client.emit('playerChat', {
|
||||
globalIndex: packet.globalIndex,
|
||||
@@ -362,7 +362,7 @@ module.exports = function (client, options) {
|
||||
plainMessage: packet.plainMessage,
|
||||
@@ -363,7 +363,7 @@ module.exports = function (client, options) {
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -38,16 +38,16 @@ index 0021870994fc59a82f0ac8aba0a65a8be43ef2f4..a53fceb843105ea2a1d88722b3fc7c3b
|
|||
options.timestamp = options.timestamp || BigInt(Date.now())
|
||||
options.salt = options.salt || 1n
|
||||
|
||||
@@ -407,7 +407,7 @@ module.exports = function (client, options) {
|
||||
@@ -405,7 +405,7 @@ module.exports = function (client, options) {
|
||||
message,
|
||||
timestamp: options.timestamp,
|
||||
salt: options.salt,
|
||||
- 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,
|
||||
offset: client._lastSeenMessages.pending,
|
||||
checksum: computeChatChecksum(client._lastSeenMessages), // 1.21.5+
|
||||
acknowledged
|
||||
@@ -422,7 +422,7 @@ module.exports = function (client, options) {
|
||||
})
|
||||
@@ -419,7 +419,7 @@ module.exports = function (client, options) {
|
||||
message,
|
||||
timestamp: options.timestamp,
|
||||
salt: options.salt,
|
||||
|
|
@ -57,7 +57,7 @@ index 0021870994fc59a82f0ac8aba0a65a8be43ef2f4..a53fceb843105ea2a1d88722b3fc7c3b
|
|||
previousMessages: client._lastSeenMessages.map((e) => ({
|
||||
messageSender: e.sender,
|
||||
diff --git a/src/client/encrypt.js b/src/client/encrypt.js
|
||||
index 63cc2bd9615100bd2fd63dfe14c094aa6b8cd1c9..36df57d1196af9761d920fa285ac48f85410eaef 100644
|
||||
index b9d21bab9faccd5dbf1975fc423fc55c73e906c5..99ffd76527b410e3a393181beb260108f4c63536 100644
|
||||
--- a/src/client/encrypt.js
|
||||
+++ b/src/client/encrypt.js
|
||||
@@ -25,7 +25,11 @@ module.exports = function (client, options) {
|
||||
|
|
@ -73,24 +73,28 @@ index 63cc2bd9615100bd2fd63dfe14c094aa6b8cd1c9..36df57d1196af9761d920fa285ac48f8
|
|||
}
|
||||
|
||||
function onJoinServerResponse (err) {
|
||||
diff --git a/src/client/pluginChannels.js b/src/client/pluginChannels.js
|
||||
index 671eb452f31e6b5fcd57d715f1009d010160c65f..7f69f511c8fb97d431ec5125c851b49be8e2ab76 100644
|
||||
--- a/src/client/pluginChannels.js
|
||||
+++ b/src/client/pluginChannels.js
|
||||
@@ -57,7 +57,7 @@ module.exports = function (client, options) {
|
||||
try {
|
||||
packet.data = proto.parsePacketBuffer(channel, packet.data).data
|
||||
} catch (error) {
|
||||
- client.emit('error', error)
|
||||
+ client.emit('error', error, { customPayload: packet })
|
||||
return
|
||||
}
|
||||
}
|
||||
diff --git a/src/client.js b/src/client.js
|
||||
index e369e77d055ba919e8f9da7b8e8b5dc879c74cf4..54bb9e6644388e9b6bd42b3012951875989cdf0c 100644
|
||||
index 74749698f8cee05b5dc749c271544f78d06645b0..e77e0a3f41c1ee780c3abbd54b0801d248c2a07c 100644
|
||||
--- a/src/client.js
|
||||
+++ b/src/client.js
|
||||
@@ -111,7 +111,13 @@ class Client extends EventEmitter {
|
||||
@@ -89,10 +89,12 @@ class Client extends EventEmitter {
|
||||
parsed.metadata.name = parsed.data.name
|
||||
parsed.data = parsed.data.params
|
||||
parsed.metadata.state = state
|
||||
- debug('read packet ' + state + '.' + parsed.metadata.name)
|
||||
- if (debug.enabled) {
|
||||
- const s = JSON.stringify(parsed.data, null, 2)
|
||||
- debug(s && s.length > 10000 ? parsed.data : s)
|
||||
+ if (!globalThis.excludeCommunicationDebugEvents?.includes(parsed.metadata.name)) {
|
||||
+ debug('read packet ' + state + '.' + parsed.metadata.name)
|
||||
+ if (debug.enabled) {
|
||||
+ const s = JSON.stringify(parsed.data, null, 2)
|
||||
+ debug(s && s.length > 10000 ? parsed.data : s)
|
||||
+ }
|
||||
}
|
||||
if (this._hasBundlePacket && parsed.metadata.name === 'bundle_delimiter') {
|
||||
if (this._mcBundle.length) { // End bundle
|
||||
@@ -110,7 +112,13 @@ class Client extends EventEmitter {
|
||||
this._hasBundlePacket = false
|
||||
}
|
||||
} else {
|
||||
|
|
@ -105,7 +109,7 @@ index e369e77d055ba919e8f9da7b8e8b5dc879c74cf4..54bb9e6644388e9b6bd42b3012951875
|
|||
}
|
||||
})
|
||||
}
|
||||
@@ -169,7 +175,10 @@ class Client extends EventEmitter {
|
||||
@@ -168,7 +176,10 @@ class Client extends EventEmitter {
|
||||
}
|
||||
|
||||
const onFatalError = (err) => {
|
||||
|
|
@ -117,21 +121,25 @@ index e369e77d055ba919e8f9da7b8e8b5dc879c74cf4..54bb9e6644388e9b6bd42b3012951875
|
|||
endSocket()
|
||||
}
|
||||
|
||||
@@ -198,6 +207,10 @@ class Client extends EventEmitter {
|
||||
@@ -197,6 +208,8 @@ class Client extends EventEmitter {
|
||||
serializer -> framer -> socket -> splitter -> deserializer */
|
||||
if (this.serializer) {
|
||||
this.serializer.end()
|
||||
+ setTimeout(() => {
|
||||
+ this.socket?.end()
|
||||
+ this.socket?.emit('end')
|
||||
+ }, 2000) // allow the serializer to finish writing
|
||||
+ this.socket?.end()
|
||||
+ this.socket?.emit('end')
|
||||
} else {
|
||||
if (this.socket) this.socket.end()
|
||||
}
|
||||
@@ -243,6 +256,7 @@ class Client extends EventEmitter {
|
||||
debug('writing packet ' + this.state + '.' + name)
|
||||
debug(params)
|
||||
}
|
||||
@@ -238,8 +251,11 @@ class Client extends EventEmitter {
|
||||
|
||||
write (name, params) {
|
||||
if (!this.serializer.writable) { return }
|
||||
- debug('writing packet ' + this.state + '.' + name)
|
||||
- debug(params)
|
||||
+ if (!globalThis.excludeCommunicationDebugEvents?.includes(name)) {
|
||||
+ debug(`[${this.state}] from ${this.isServer ? 'server' : 'client'}: ` + name)
|
||||
+ debug(params)
|
||||
+ }
|
||||
+ this.emit('writePacket', name, params)
|
||||
this.serializer.write({ name, params })
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
diff --git a/fonts/pixelart-icons-font.css b/fonts/pixelart-icons-font.css
|
||||
index 3b2ebe839370d96bf93ef5ca94a827f07e49378d..4f8d76be2ca6e4ddc43c68d0a6f0f69979165ab4 100644
|
||||
index 3b2ebe839370d96bf93ef5ca94a827f07e49378d..103ab4d6b9f3b5c9f41d1407e3cbf4ac392fbf41 100644
|
||||
--- a/fonts/pixelart-icons-font.css
|
||||
+++ b/fonts/pixelart-icons-font.css
|
||||
@@ -1,16 +1,13 @@
|
||||
|
|
@ -10,11 +10,10 @@ index 3b2ebe839370d96bf93ef5ca94a827f07e49378d..4f8d76be2ca6e4ddc43c68d0a6f0f699
|
|||
+ src:
|
||||
url("pixelart-icons-font.woff2?t=1711815892278") format("woff2"),
|
||||
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.ttf?t=1711815892278') format('truetype'); /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
|
||||
}
|
||||
|
||||
|
||||
[class^="pixelart-icons-font-"], [class*=" pixelart-icons-font-"] {
|
||||
font-family: 'pixelart-icons-font' !important;
|
||||
- font-size:24px;
|
||||
|
|
|
|||
16
patches/three@0.154.0.patch
Normal file
16
patches/three@0.154.0.patch
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
diff --git a/examples/jsm/webxr/VRButton.js b/examples/jsm/webxr/VRButton.js
|
||||
index 6856a21b17aa45d7922bbf776fd2d7e63c7a9b4e..0925b706f7629bd52f0bb5af469536af8f5fce2c 100644
|
||||
--- a/examples/jsm/webxr/VRButton.js
|
||||
+++ b/examples/jsm/webxr/VRButton.js
|
||||
@@ -62,7 +62,10 @@ class VRButton {
|
||||
// ('local' is always available for immersive sessions and doesn't need to
|
||||
// be requested separately.)
|
||||
|
||||
- const sessionInit = { optionalFeatures: [ 'local-floor', 'bounded-floor', 'hand-tracking', 'layers' ] };
|
||||
+ const sessionInit = {
|
||||
+ optionalFeatures: ['local-floor', 'bounded-floor', 'layers'],
|
||||
+ domOverlay: { root: document.body },
|
||||
+ };
|
||||
navigator.xr.requestSession( 'immersive-vr', sessionInit ).then( onSessionStarted );
|
||||
|
||||
} else {
|
||||
11203
pnpm-lock.yaml
generated
11203
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -22,7 +22,7 @@ const buildOptions = {
|
|||
},
|
||||
platform: 'browser',
|
||||
entryPoints: [path.join(__dirname, './viewer/lib/mesher/mesher.ts')],
|
||||
minify: !watch,
|
||||
minify: true,
|
||||
logLevel: 'info',
|
||||
drop: !watch ? [
|
||||
'debugger'
|
||||
|
|
@ -35,15 +35,11 @@ const buildOptions = {
|
|||
define: {
|
||||
'process.env.BROWSER': '"true"',
|
||||
},
|
||||
loader: {
|
||||
'.png': 'dataurl',
|
||||
'.obj': 'text'
|
||||
},
|
||||
plugins: [
|
||||
...mesherSharedPlugins,
|
||||
{
|
||||
name: 'external-json',
|
||||
setup(build) {
|
||||
setup (build) {
|
||||
build.onResolve({ filter: /\.json$/ }, args => {
|
||||
const fileName = args.path.split('/').pop().replace('.json', '')
|
||||
if (args.resolveDir.includes('minecraft-data')) {
|
||||
|
|
|
|||
|
|
@ -11,17 +11,11 @@
|
|||
|
||||
html, body {
|
||||
height: 100%;
|
||||
touch-action: none;
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
* {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
canvas {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
|
|
|||
|
|
@ -1,170 +0,0 @@
|
|||
import { EntityMesh, rendererSpecialHandled, EntityDebugFlags } from '../viewer/three/entity/EntityMesh'
|
||||
|
||||
export const displayEntitiesDebugList = (version: string) => {
|
||||
// Create results container
|
||||
const container = document.createElement('div')
|
||||
container.style.cssText = `
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
background: rgba(0,0,0,0.8);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
font-family: monospace;
|
||||
min-width: 400px;
|
||||
z-index: 1000;
|
||||
`
|
||||
document.body.appendChild(container)
|
||||
|
||||
// Add title
|
||||
const title = document.createElement('h2')
|
||||
title.textContent = 'Minecraft Entity Support'
|
||||
title.style.cssText = 'margin-top: 0; text-align: center;'
|
||||
container.appendChild(title)
|
||||
|
||||
// Test entities
|
||||
const results: Array<{
|
||||
entity: string;
|
||||
supported: boolean;
|
||||
type?: 'obj' | 'bedrock' | 'special';
|
||||
mappedFrom?: string;
|
||||
textureMap?: boolean;
|
||||
errors?: string[];
|
||||
}> = []
|
||||
const { mcData } = window
|
||||
const entityNames = Object.keys(mcData.entitiesArray.reduce((acc, entity) => {
|
||||
acc[entity.name] = true
|
||||
return acc
|
||||
}, {}))
|
||||
|
||||
// Add loading indicator
|
||||
const loading = document.createElement('div')
|
||||
loading.textContent = 'Testing entities...'
|
||||
loading.style.textAlign = 'center'
|
||||
container.appendChild(loading)
|
||||
|
||||
for (const entity of entityNames) {
|
||||
const debugFlags: EntityDebugFlags = {}
|
||||
|
||||
if (rendererSpecialHandled.includes(entity)) {
|
||||
results.push({
|
||||
entity,
|
||||
supported: true,
|
||||
type: 'special',
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const { mesh: entityMesh } = new EntityMesh(version, entity, undefined, {}, debugFlags)
|
||||
// find the most distant pos child
|
||||
window.objects ??= {}
|
||||
window.objects[entity] = entityMesh
|
||||
|
||||
results.push({
|
||||
entity,
|
||||
supported: !!debugFlags.type || rendererSpecialHandled.includes(entity),
|
||||
type: debugFlags.type,
|
||||
mappedFrom: debugFlags.tempMap,
|
||||
textureMap: debugFlags.textureMap,
|
||||
errors: debugFlags.errors
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
results.push({
|
||||
entity,
|
||||
supported: false,
|
||||
mappedFrom: debugFlags.tempMap
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Remove loading indicator
|
||||
loading.remove()
|
||||
|
||||
const createSection = (title: string, items: any[], filter: (item: any) => boolean) => {
|
||||
const section = document.createElement('div')
|
||||
section.style.marginBottom = '20px'
|
||||
|
||||
const sectionTitle = document.createElement('h3')
|
||||
sectionTitle.textContent = title
|
||||
sectionTitle.style.textAlign = 'center'
|
||||
section.appendChild(sectionTitle)
|
||||
|
||||
const list = document.createElement('ul')
|
||||
list.style.cssText = 'padding-left: 20px; list-style-type: none; margin: 0;'
|
||||
|
||||
const filteredItems = items.filter(filter)
|
||||
for (const item of filteredItems) {
|
||||
const listItem = document.createElement('li')
|
||||
listItem.style.cssText = 'line-height: 1.4; margin: 8px 0;'
|
||||
|
||||
const entityName = document.createElement('strong')
|
||||
entityName.style.cssText = 'user-select: text;-webkit-user-select: text;'
|
||||
entityName.textContent = item.entity
|
||||
listItem.appendChild(entityName)
|
||||
|
||||
let text = ''
|
||||
if (item.mappedFrom) {
|
||||
text += ` -> ${item.mappedFrom}`
|
||||
}
|
||||
if (item.type) {
|
||||
text += ` - ${item.type}`
|
||||
}
|
||||
if (item.textureMap) {
|
||||
text += ' ⚠️'
|
||||
}
|
||||
if (item.errors) {
|
||||
text += ' ❌'
|
||||
}
|
||||
|
||||
listItem.appendChild(document.createTextNode(text))
|
||||
list.appendChild(listItem)
|
||||
}
|
||||
|
||||
section.appendChild(list)
|
||||
return { section, count: filteredItems.length }
|
||||
}
|
||||
|
||||
// Sort results - bedrock first
|
||||
results.sort((a, b) => {
|
||||
if (a.type === 'bedrock' && b.type !== 'bedrock') return -1
|
||||
if (a.type !== 'bedrock' && b.type === 'bedrock') return 1
|
||||
return a.entity.localeCompare(b.entity)
|
||||
})
|
||||
|
||||
// Add sections
|
||||
const sections = [
|
||||
{
|
||||
title: '❌ Unsupported Entities',
|
||||
filter: (r: any) => !r.supported && !r.mappedFrom
|
||||
},
|
||||
{
|
||||
title: '⚠️ Partially Supported Entities',
|
||||
filter: (r: any) => r.mappedFrom
|
||||
},
|
||||
{
|
||||
title: '✅ Supported Entities',
|
||||
filter: (r: any) => r.supported && !r.mappedFrom
|
||||
}
|
||||
]
|
||||
|
||||
for (const { title, filter } of sections) {
|
||||
const { section, count } = createSection(title, results, filter)
|
||||
if (count > 0) {
|
||||
container.appendChild(section)
|
||||
}
|
||||
}
|
||||
|
||||
// log object with errors per entity
|
||||
const errors = results.filter(r => r.errors).map(r => ({
|
||||
entity: r.entity,
|
||||
errors: r.errors
|
||||
}))
|
||||
console.log(errors)
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
//@ts-nocheck
|
||||
import { Vec3 } from 'vec3'
|
||||
import * as THREE from 'three'
|
||||
import '../../src/getCollisionShapes'
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
if (!new URL(location.href).searchParams.get('playground')) location.href = '/?playground=true'
|
||||
// import { BasePlaygroundScene } from './baseScene'
|
||||
// import { playgroundGlobalUiState } from './playgroundUi'
|
||||
// import * as scenes from './scenes'
|
||||
import { BasePlaygroundScene } from './baseScene'
|
||||
import { playgroundGlobalUiState } from './playgroundUi'
|
||||
import * as scenes from './scenes'
|
||||
|
||||
// const qsScene = new URLSearchParams(window.location.search).get('scene')
|
||||
// const Scene: typeof BasePlaygroundScene = qsScene ? scenes[qsScene] : scenes.main
|
||||
// playgroundGlobalUiState.scenes = ['main', 'railsCobweb', 'floorRandom', 'lightingStarfield', 'transparencyIssue', 'entities', 'frequentUpdates', 'slabsOptimization', 'allEntities']
|
||||
// playgroundGlobalUiState.selected = qsScene ?? 'main'
|
||||
const qsScene = new URLSearchParams(window.location.search).get('scene')
|
||||
const Scene: typeof BasePlaygroundScene = qsScene ? scenes[qsScene] : scenes.main
|
||||
playgroundGlobalUiState.scenes = ['main', 'railsCobweb', 'floorRandom', 'lightingStarfield', 'transparencyIssue', 'entities', 'frequentUpdates', 'slabsOptimization', 'allEntities']
|
||||
playgroundGlobalUiState.selected = qsScene ?? 'main'
|
||||
|
||||
// const scene = new Scene()
|
||||
// globalThis.scene = scene
|
||||
const scene = new Scene()
|
||||
globalThis.scene = scene
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { BasePlaygroundScene } from '../baseScene'
|
||||
import { EntityDebugFlags, EntityMesh, rendererSpecialHandled } from '../../viewer/three/entity/EntityMesh'
|
||||
import { displayEntitiesDebugList } from '../allEntitiesDebug'
|
||||
import { EntityDebugFlags, EntityMesh, rendererSpecialHandled } from '../../viewer/lib/entity/EntityMesh'
|
||||
|
||||
export default class AllEntities extends BasePlaygroundScene {
|
||||
continuousRender = false
|
||||
|
|
@ -8,6 +7,159 @@ export default class AllEntities extends BasePlaygroundScene {
|
|||
|
||||
async initData () {
|
||||
await super.initData()
|
||||
displayEntitiesDebugList(this.version)
|
||||
|
||||
// Create results container
|
||||
const container = document.createElement('div')
|
||||
container.style.cssText = `
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
background: rgba(0,0,0,0.8);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
font-family: monospace;
|
||||
min-width: 400px;
|
||||
`
|
||||
document.body.appendChild(container)
|
||||
|
||||
// Add title
|
||||
const title = document.createElement('h2')
|
||||
title.textContent = 'Minecraft Entity Support'
|
||||
title.style.cssText = 'margin-top: 0; text-align: center;'
|
||||
container.appendChild(title)
|
||||
|
||||
// Test entities
|
||||
const results: Array<{
|
||||
entity: string;
|
||||
supported: boolean;
|
||||
type?: 'obj' | 'bedrock';
|
||||
mappedFrom?: string;
|
||||
textureMap?: boolean;
|
||||
errors?: string[];
|
||||
}> = []
|
||||
const { mcData } = window
|
||||
const entityNames = Object.keys(mcData.entitiesArray.reduce((acc, entity) => {
|
||||
acc[entity.name] = true
|
||||
return acc
|
||||
}, {}))
|
||||
|
||||
// Add loading indicator
|
||||
const loading = document.createElement('div')
|
||||
loading.textContent = 'Testing entities...'
|
||||
loading.style.textAlign = 'center'
|
||||
container.appendChild(loading)
|
||||
|
||||
for (const entity of entityNames) {
|
||||
const debugFlags: EntityDebugFlags = {}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new EntityMesh(this.version, entity, viewer.world, {}, debugFlags)
|
||||
|
||||
results.push({
|
||||
entity,
|
||||
supported: !!debugFlags.type || rendererSpecialHandled.includes(entity),
|
||||
type: debugFlags.type,
|
||||
mappedFrom: debugFlags.tempMap,
|
||||
textureMap: debugFlags.textureMap,
|
||||
errors: debugFlags.errors
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
results.push({
|
||||
entity,
|
||||
supported: false,
|
||||
mappedFrom: debugFlags.tempMap
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Remove loading indicator
|
||||
loading.remove()
|
||||
|
||||
const createSection = (title: string, items: any[], filter: (item: any) => boolean) => {
|
||||
const section = document.createElement('div')
|
||||
section.style.marginBottom = '20px'
|
||||
|
||||
const sectionTitle = document.createElement('h3')
|
||||
sectionTitle.textContent = title
|
||||
sectionTitle.style.textAlign = 'center'
|
||||
section.appendChild(sectionTitle)
|
||||
|
||||
const list = document.createElement('ul')
|
||||
list.style.cssText = 'padding-left: 20px; list-style-type: none; margin: 0;'
|
||||
|
||||
const filteredItems = items.filter(filter)
|
||||
for (const item of filteredItems) {
|
||||
const listItem = document.createElement('li')
|
||||
listItem.style.cssText = 'line-height: 1.4; margin: 8px 0;'
|
||||
|
||||
const entityName = document.createElement('strong')
|
||||
entityName.style.cssText = 'user-select: text;-webkit-user-select: text;'
|
||||
entityName.textContent = item.entity
|
||||
listItem.appendChild(entityName)
|
||||
|
||||
let text = ''
|
||||
if (item.mappedFrom) {
|
||||
text += ` -> ${item.mappedFrom}`
|
||||
}
|
||||
if (item.type) {
|
||||
text += ` - ${item.type}`
|
||||
}
|
||||
if (item.textureMap) {
|
||||
text += ' ⚠️'
|
||||
}
|
||||
if (item.errors) {
|
||||
text += ' ❌'
|
||||
}
|
||||
|
||||
listItem.appendChild(document.createTextNode(text))
|
||||
list.appendChild(listItem)
|
||||
}
|
||||
|
||||
section.appendChild(list)
|
||||
return { section, count: filteredItems.length }
|
||||
}
|
||||
|
||||
// Sort results - bedrock first
|
||||
results.sort((a, b) => {
|
||||
if (a.type === 'bedrock' && b.type !== 'bedrock') return -1
|
||||
if (a.type !== 'bedrock' && b.type === 'bedrock') return 1
|
||||
return a.entity.localeCompare(b.entity)
|
||||
})
|
||||
|
||||
// Add sections
|
||||
const sections = [
|
||||
{
|
||||
title: '❌ Unsupported Entities',
|
||||
filter: (r: any) => !r.supported && !r.mappedFrom
|
||||
},
|
||||
{
|
||||
title: '⚠️ Partially Supported Entities',
|
||||
filter: (r: any) => r.mappedFrom
|
||||
},
|
||||
{
|
||||
title: '✅ Supported Entities',
|
||||
filter: (r: any) => r.supported && !r.mappedFrom
|
||||
}
|
||||
]
|
||||
|
||||
for (const { title, filter } of sections) {
|
||||
const { section, count } = createSection(title, results, filter)
|
||||
if (count > 0) {
|
||||
container.appendChild(section)
|
||||
}
|
||||
}
|
||||
|
||||
// log object with errors per entity
|
||||
const errors = results.filter(r => r.errors).map(r => ({
|
||||
entity: r.entity,
|
||||
errors: r.errors
|
||||
}))
|
||||
console.log(errors)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
//@ts-nocheck
|
||||
import * as THREE from 'three'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { BasePlaygroundScene } from '../baseScene'
|
||||
import { WorldRendererThree } from '../../viewer/three/worldrendererThree'
|
||||
import { WorldRendererThree } from '../../viewer/lib/worldrendererThree'
|
||||
|
||||
export default class extends BasePlaygroundScene {
|
||||
continuousRender = true
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
//@ts-nocheck
|
||||
import { Vec3 } from 'vec3'
|
||||
import { BasePlaygroundScene } from '../baseScene'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
//@ts-nocheck
|
||||
import * as THREE from 'three'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { BasePlaygroundScene } from '../baseScene'
|
||||
import { WorldRendererThree } from '../../viewer/three/worldrendererThree'
|
||||
import { WorldRendererThree } from '../../viewer/lib/worldrendererThree'
|
||||
|
||||
export default class extends BasePlaygroundScene {
|
||||
continuousRender = true
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
//@ts-nocheck
|
||||
// eslint-disable-next-line import/no-named-as-default
|
||||
import GUI, { Controller } from 'lil-gui'
|
||||
import * as THREE from 'three'
|
||||
import JSZip from 'jszip'
|
||||
import { BasePlaygroundScene } from '../baseScene'
|
||||
import { TWEEN_DURATION } from '../../viewer/three/entities'
|
||||
import { EntityMesh } from '../../viewer/three/entity/EntityMesh'
|
||||
import { TWEEN_DURATION } from '../../viewer/lib/entities'
|
||||
import { EntityMesh } from '../../viewer/lib/entity/EntityMesh'
|
||||
|
||||
class MainScene extends BasePlaygroundScene {
|
||||
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
||||
|
|
@ -174,6 +173,7 @@ class MainScene extends BasePlaygroundScene {
|
|||
canvas.height = size
|
||||
renderer.setSize(size, size)
|
||||
|
||||
//@ts-expect-error
|
||||
viewer.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 10)
|
||||
viewer.scene.background = null
|
||||
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ function getAllMethods (obj) {
|
|||
return [...methods] as string[]
|
||||
}
|
||||
|
||||
export const delayedIterator = async <T> (arr: T[], delay: number, exec: (item: T, index: number) => Promise<void>, chunkSize = 1) => {
|
||||
export const delayedIterator = async <T> (arr: T[], delay: number, exec: (item: T, index: number) => void, chunkSize = 1) => {
|
||||
// if delay is 0 then don't use setTimeout
|
||||
for (let i = 0; i < arr.length; i += chunkSize) {
|
||||
if (delay) {
|
||||
|
|
@ -74,6 +74,6 @@ export const delayedIterator = async <T> (arr: T[], delay: number, exec: (item:
|
|||
setTimeout(resolve, delay)
|
||||
})
|
||||
}
|
||||
await exec(arr[i], i)
|
||||
exec(arr[i], i)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { defineConfig, ModifyRspackConfigUtils } from '@rsbuild/core';
|
|||
import { pluginNodePolyfill } from '@rsbuild/plugin-node-polyfill';
|
||||
import { pluginReact } from '@rsbuild/plugin-react';
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
|
||||
export const appAndRendererSharedConfig = () => defineConfig({
|
||||
dev: {
|
||||
|
|
@ -61,24 +60,18 @@ export const appAndRendererSharedConfig = () => defineConfig({
|
|||
],
|
||||
tools: {
|
||||
rspack (config, helpers) {
|
||||
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8'))
|
||||
const hasFileProtocol = Object.values(packageJson.pnpm.overrides).some((dep) => (dep as string).startsWith('file:'))
|
||||
if (hasFileProtocol) {
|
||||
// enable node_modules watching
|
||||
config.watchOptions.ignored = /\.git/
|
||||
}
|
||||
rspackViewerConfig(config, helpers)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const rspackViewerConfig = (config, { appendPlugins, addRules, rspack }: ModifyRspackConfigUtils) => {
|
||||
appendPlugins(new rspack.NormalModuleReplacementPlugin(/data|prismarine-physics/, (resource) => {
|
||||
appendPlugins(new rspack.NormalModuleReplacementPlugin(/data/, (resource) => {
|
||||
let absolute: string
|
||||
const request = resource.request.replaceAll('\\', '/')
|
||||
absolute = path.join(resource.context, request).replaceAll('\\', '/')
|
||||
if (request.includes('minecraft-data/data/pc/1.') || request.includes('prismarine-physics')) {
|
||||
console.log('Error: incompatible resource', request, 'from', resource.contextInfo.issuer)
|
||||
if (request.includes('minecraft-data/data/pc/1.')) {
|
||||
console.log('Error: incompatible resource', request, resource.contextInfo.issuer)
|
||||
process.exit(1)
|
||||
// throw new Error(`${resource.request} was requested by ${resource.contextInfo.issuer}`)
|
||||
}
|
||||
|
|
@ -108,10 +101,6 @@ export const rspackViewerConfig = (config, { appendPlugins, addRules, rspack }:
|
|||
{
|
||||
test: /\.txt$/,
|
||||
type: 'asset/source',
|
||||
},
|
||||
{
|
||||
test: /\.log$/,
|
||||
type: 'asset/source',
|
||||
}
|
||||
])
|
||||
config.ignoreWarnings = [
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
import { proxy } from 'valtio'
|
||||
import { NonReactiveState, RendererReactiveState } from '../../src/appViewer'
|
||||
|
||||
export const getDefaultRendererState = (): {
|
||||
reactive: RendererReactiveState
|
||||
nonReactive: NonReactiveState
|
||||
} => {
|
||||
return {
|
||||
reactive: proxy({
|
||||
world: {
|
||||
chunksLoaded: new Set(),
|
||||
heightmaps: new Map(),
|
||||
allChunksLoaded: true,
|
||||
mesherWork: false,
|
||||
intersectMedia: null
|
||||
},
|
||||
renderer: '',
|
||||
preventEscapeMenu: false
|
||||
}),
|
||||
nonReactive: {
|
||||
world: {
|
||||
chunksLoaded: new Set(),
|
||||
chunksTotalNumber: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
6
renderer/viewer/index.js
Normal file
6
renderer/viewer/index.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
Viewer: require('./lib/viewer').Viewer,
|
||||
WorldDataEmitter: require('./lib/worldDataEmitter').WorldDataEmitter,
|
||||
Entity: require('./lib/entity/EntityMesh'),
|
||||
getBufferFromStream: require('./lib/simpleUtils').getBufferFromStream
|
||||
}
|
||||
|
|
@ -1,87 +1,88 @@
|
|||
import { EventEmitter } from 'events'
|
||||
import { Vec3 } from 'vec3'
|
||||
import TypedEmitter from 'typed-emitter'
|
||||
import { ItemSelector } from 'mc-assets/dist/itemDefinitions'
|
||||
import { GameMode, Team } from 'mineflayer'
|
||||
import { proxy } from 'valtio'
|
||||
import type { HandItemBlock } from '../three/holdingBlock'
|
||||
import { HandItemBlock } from './holdingBlock'
|
||||
|
||||
export type MovementState = 'NOT_MOVING' | 'WALKING' | 'SPRINTING' | 'SNEAKING'
|
||||
export type ItemSpecificContextProperties = Partial<Pick<ItemSelector['properties'], 'minecraft:using_item' | 'minecraft:use_duration' | 'minecraft:use_cycle' | 'minecraft:display_context'>>
|
||||
export type CameraPerspective = 'first_person' | 'third_person_back' | 'third_person_front'
|
||||
|
||||
export type BlockShape = { position: any; width: any; height: any; depth: any; }
|
||||
export type BlocksShapes = BlockShape[]
|
||||
|
||||
// edit src/mineflayer/playerState.ts for implementation of player state from mineflayer
|
||||
export const getInitialPlayerState = () => proxy({
|
||||
playerSkin: undefined as string | undefined,
|
||||
inWater: false,
|
||||
waterBreathing: false,
|
||||
backgroundColor: [0, 0, 0] as [number, number, number],
|
||||
ambientLight: 0,
|
||||
directionalLight: 0,
|
||||
eyeHeight: 0,
|
||||
gameMode: undefined as GameMode | undefined,
|
||||
lookingAtBlock: undefined as {
|
||||
x: number
|
||||
y: number
|
||||
z: number
|
||||
face?: number
|
||||
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,
|
||||
export type PlayerStateEvents = {
|
||||
heldItemChanged: (item: HandItemBlock | undefined, isLeftHand: boolean) => void
|
||||
}
|
||||
|
||||
cameraSpectatingEntity: undefined as number | undefined,
|
||||
export interface IPlayerState {
|
||||
getEyeHeight(): number
|
||||
getMovementState(): MovementState
|
||||
getVelocity(): Vec3
|
||||
isOnGround(): boolean
|
||||
isSneaking(): boolean
|
||||
isFlying(): boolean
|
||||
isSprinting (): boolean
|
||||
getItemUsageTicks?(): number
|
||||
// isUsingItem?(): boolean
|
||||
getHeldItem?(isLeftHand: boolean): HandItemBlock | undefined
|
||||
username?: string
|
||||
onlineMode?: boolean
|
||||
|
||||
team: undefined as Team | undefined,
|
||||
})
|
||||
events: TypedEmitter<PlayerStateEvents>
|
||||
|
||||
export const getPlayerStateUtils = (reactive: PlayerStateReactive) => ({
|
||||
isSpectator () {
|
||||
return reactive.gameMode === 'spectator'
|
||||
},
|
||||
isSpectatingEntity () {
|
||||
return reactive.cameraSpectatingEntity !== undefined && reactive.gameMode === 'spectator'
|
||||
},
|
||||
isThirdPerson () {
|
||||
if ((this as PlayerStateUtils).isSpectatingEntity()) return false
|
||||
return reactive.perspective === 'third_person_back' || reactive.perspective === 'third_person_front'
|
||||
}
|
||||
})
|
||||
|
||||
export const getInitialPlayerStateRenderer = () => ({
|
||||
reactive: getInitialPlayerState()
|
||||
})
|
||||
|
||||
export type PlayerStateReactive = ReturnType<typeof getInitialPlayerState>
|
||||
export type PlayerStateUtils = ReturnType<typeof getPlayerStateUtils>
|
||||
|
||||
export type PlayerStateRenderer = PlayerStateReactive
|
||||
|
||||
export const getItemSelector = (playerState: PlayerStateRenderer, specificProperties: ItemSpecificContextProperties, item?: import('prismarine-item').Item) => {
|
||||
return {
|
||||
...specificProperties,
|
||||
'minecraft:date': new Date(),
|
||||
// "minecraft:context_dimension": bot.entityp,
|
||||
// 'minecraft:time': bot.time.timeOfDay / 24_000,
|
||||
reactive: {
|
||||
playerSkin: string | undefined
|
||||
}
|
||||
}
|
||||
|
||||
export class BasePlayerState implements IPlayerState {
|
||||
reactive = proxy({
|
||||
playerSkin: undefined
|
||||
})
|
||||
protected movementState: MovementState = 'NOT_MOVING'
|
||||
protected velocity = new Vec3(0, 0, 0)
|
||||
protected onGround = true
|
||||
protected sneaking = false
|
||||
protected flying = false
|
||||
protected sprinting = false
|
||||
readonly events = new EventEmitter() as TypedEmitter<PlayerStateEvents>
|
||||
|
||||
getEyeHeight (): number {
|
||||
return 1.62
|
||||
}
|
||||
|
||||
getMovementState (): MovementState {
|
||||
return this.movementState
|
||||
}
|
||||
|
||||
getVelocity (): Vec3 {
|
||||
return this.velocity
|
||||
}
|
||||
|
||||
isOnGround (): boolean {
|
||||
return this.onGround
|
||||
}
|
||||
|
||||
isSneaking (): boolean {
|
||||
return this.sneaking
|
||||
}
|
||||
|
||||
isFlying (): boolean {
|
||||
return this.flying
|
||||
}
|
||||
|
||||
isSprinting (): boolean {
|
||||
return this.sprinting
|
||||
}
|
||||
|
||||
// For testing purposes
|
||||
setState (state: Partial<{
|
||||
movementState: MovementState
|
||||
velocity: Vec3
|
||||
onGround: boolean
|
||||
sneaking: boolean
|
||||
flying: boolean
|
||||
sprinting: boolean
|
||||
}>) {
|
||||
Object.assign(this, state)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
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)
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -2,12 +2,11 @@ import * as THREE from 'three'
|
|||
import { OBJLoader } from 'three-stdlib'
|
||||
import huskPng from 'mc-assets/dist/other-textures/latest/entity/zombie/husk.png'
|
||||
import { Vec3 } from 'vec3'
|
||||
import ocelotPng from '../../../../node_modules/mc-assets/dist/other-textures/latest/entity/cat/ocelot.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 tippedArrowTexture from '../../../../node_modules/mc-assets/dist/other-textures/1.21.2/entity/projectiles/tipped_arrow.png'
|
||||
import { loadTexture } from '../threeJsUtils'
|
||||
import { WorldRendererThree } from '../worldrendererThree'
|
||||
import { WorldRendererCommon } from '../worldrendererCommon'
|
||||
import { loadTexture } from '../utils'
|
||||
import entities from './entities.json'
|
||||
import { externalModels } from './objModels'
|
||||
import externalTexturesJson from './externalTextures.json'
|
||||
|
|
@ -224,7 +223,7 @@ function addCube (
|
|||
}
|
||||
|
||||
export function getMesh (
|
||||
worldRenderer: WorldRendererThree | undefined,
|
||||
worldRenderer: WorldRendererCommon | undefined,
|
||||
texture: string,
|
||||
jsonModel: JsonModel,
|
||||
overrides: EntityOverrides = {},
|
||||
|
|
@ -238,11 +237,10 @@ export function getMesh (
|
|||
if (useBlockTexture) {
|
||||
if (!worldRenderer) throw new Error('worldRenderer is required for block textures')
|
||||
const blockName = texture.slice(6)
|
||||
const textureInfo = worldRenderer.resourcesManager.currentResources.blocksAtlasJson.textures[blockName]
|
||||
const textureInfo = worldRenderer.blocksAtlasParser!.getTextureInfo(blockName)
|
||||
if (textureInfo) {
|
||||
textureWidth = blocksTexture?.image.width ?? textureWidth
|
||||
textureHeight = blocksTexture?.image.height ?? textureHeight
|
||||
// todo support su/sv
|
||||
textureWidth = blocksTexture!.image.width
|
||||
textureHeight = blocksTexture!.image.height
|
||||
textureOffset = [textureInfo.u, textureInfo.v]
|
||||
} else {
|
||||
console.error(`Unknown block ${blockName}`)
|
||||
|
|
@ -439,7 +437,7 @@ export class EntityMesh {
|
|||
constructor (
|
||||
version: string,
|
||||
type: string,
|
||||
worldRenderer?: WorldRendererThree,
|
||||
worldRenderer?: WorldRendererCommon,
|
||||
overrides: EntityOverrides = {},
|
||||
debugFlags: EntityDebugFlags = {}
|
||||
) {
|
||||
|
|
@ -458,7 +456,7 @@ export class EntityMesh {
|
|||
'skeleton_horse': `textures/${version}/entity/horse/horse_skeleton.png`,
|
||||
'donkey': `textures/${version}/entity/horse/donkey.png`,
|
||||
'mule': `textures/${version}/entity/horse/mule.png`,
|
||||
'ocelot': ocelotPng,
|
||||
'ocelot': `textures/${version}/entity/cat/ocelot.png`,
|
||||
'arrow': arrowTexture,
|
||||
'spectral_arrow': spectralArrowTexture,
|
||||
'tipped_arrow': tippedArrowTexture
|
||||
|
|
@ -529,6 +527,12 @@ export class EntityMesh {
|
|||
debugFlags)
|
||||
mesh.name = `geometry_${name}`
|
||||
this.mesh.add(mesh)
|
||||
|
||||
const skeletonHelper = new THREE.SkeletonHelper(mesh)
|
||||
//@ts-expect-error
|
||||
skeletonHelper.material.linewidth = 2
|
||||
skeletonHelper.visible = false
|
||||
this.mesh.add(skeletonHelper)
|
||||
}
|
||||
debugFlags.type = 'bedrock'
|
||||
}
|
||||
|
|
@ -547,4 +551,3 @@ export class EntityMesh {
|
|||
}
|
||||
}
|
||||
}
|
||||
globalThis.EntityMesh = EntityMesh
|
||||
103
renderer/viewer/lib/entity/animations.js
Normal file
103
renderer/viewer/lib/entity/animations.js
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import { PlayerAnimation } from 'skinview3d'
|
||||
|
||||
export class WalkingGeneralSwing extends PlayerAnimation {
|
||||
|
||||
switchAnimationCallback
|
||||
|
||||
isRunning = false
|
||||
isMoving = true
|
||||
|
||||
_startArmSwing
|
||||
|
||||
swingArm() {
|
||||
this._startArmSwing = this.progress
|
||||
}
|
||||
|
||||
animate(player) {
|
||||
// Multiply by animation's natural speed
|
||||
let t
|
||||
const updateT = () => {
|
||||
if (!this.isMoving) {
|
||||
t = 0
|
||||
return
|
||||
}
|
||||
if (this.isRunning) {
|
||||
t = this.progress * 10 + Math.PI * 0.5
|
||||
} else {
|
||||
t = this.progress * 8
|
||||
}
|
||||
}
|
||||
updateT()
|
||||
let reset = false
|
||||
|
||||
if ((this.isRunning ? Math.cos(t) : Math.sin(t)) < 0.01) {
|
||||
if (this.switchAnimationCallback) {
|
||||
reset = true
|
||||
this.progress = 0
|
||||
updateT()
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isRunning) {
|
||||
// Leg swing with larger amplitude
|
||||
player.skin.leftLeg.rotation.x = Math.cos(t + Math.PI) * 1.3
|
||||
player.skin.rightLeg.rotation.x = Math.cos(t) * 1.3
|
||||
} else {
|
||||
// Leg swing
|
||||
player.skin.leftLeg.rotation.x = Math.sin(t) * 0.5
|
||||
player.skin.rightLeg.rotation.x = Math.sin(t + Math.PI) * 0.5
|
||||
}
|
||||
|
||||
if (this._startArmSwing) {
|
||||
const tHand = (this.progress - this._startArmSwing) * 18 + Math.PI * 0.5
|
||||
player.skin.rightArm.rotation.x = Math.cos(tHand) * 1.5
|
||||
const basicArmRotationZ = Math.PI * 0.1
|
||||
player.skin.rightArm.rotation.z = Math.cos(t + Math.PI) * 0.3 - basicArmRotationZ
|
||||
|
||||
if (tHand > Math.PI + Math.PI * 0.5) {
|
||||
this._startArmSwing = null
|
||||
player.skin.rightArm.rotation.z = 0
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isRunning) {
|
||||
player.skin.leftArm.rotation.x = Math.cos(t) * 1.5
|
||||
if (!this._startArmSwing) {
|
||||
player.skin.rightArm.rotation.x = Math.cos(t + Math.PI) * 1.5
|
||||
}
|
||||
const basicArmRotationZ = Math.PI * 0.1
|
||||
player.skin.leftArm.rotation.z = Math.cos(t) * 0.1 + basicArmRotationZ
|
||||
if (!this._startArmSwing) {
|
||||
player.skin.rightArm.rotation.z = Math.cos(t + Math.PI) * 0.1 - basicArmRotationZ
|
||||
}
|
||||
} else {
|
||||
// Arm swing
|
||||
player.skin.leftArm.rotation.x = Math.sin(t + Math.PI) * 0.5
|
||||
if (!this._startArmSwing) {
|
||||
player.skin.rightArm.rotation.x = Math.sin(t) * 0.5
|
||||
}
|
||||
const basicArmRotationZ = Math.PI * 0.02
|
||||
player.skin.leftArm.rotation.z = Math.cos(t) * 0.03 + basicArmRotationZ
|
||||
if (!this._startArmSwing) {
|
||||
player.skin.rightArm.rotation.z = Math.cos(t + Math.PI) * 0.03 - basicArmRotationZ
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isRunning) {
|
||||
player.rotation.z = Math.cos(t + Math.PI) * 0.01
|
||||
}
|
||||
if (this.isRunning) {
|
||||
const basicCapeRotationX = Math.PI * 0.3
|
||||
player.cape.rotation.x = Math.sin(t * 2) * 0.1 + basicCapeRotationX
|
||||
} else {
|
||||
// Always add an angle for cape around the x axis
|
||||
const basicCapeRotationX = Math.PI * 0.06
|
||||
player.cape.rotation.x = Math.sin(t / 1.5) * 0.06 + basicCapeRotationX
|
||||
}
|
||||
|
||||
if (reset) {
|
||||
this.switchAnimationCallback()
|
||||
this.switchAnimationCallback = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,6 @@ 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 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 const armorTextures = {
|
||||
|
|
@ -2,22 +2,27 @@
|
|||
import { ItemRenderer, Identifier, ItemStack, NbtString, Structure, StructureRenderer, ItemRendererResources, BlockDefinition, BlockModel, TextureAtlas, Resources, ItemModel } from 'deepslate'
|
||||
import { mat4, vec3 } from 'gl-matrix'
|
||||
import { AssetsParser } from 'mc-assets/dist/assetsParser'
|
||||
import { getLoadedImage, versionToNumber } from 'mc-assets/dist/utils'
|
||||
import { getLoadedImage } from 'mc-assets/dist/utils'
|
||||
import { BlockModel as BlockModelMcAssets, AtlasParser } from 'mc-assets'
|
||||
import { getLoadedBlockstatesStore, getLoadedModelsStore } from 'mc-assets/dist/stores'
|
||||
import { makeTextureAtlas } from 'mc-assets/dist/atlasCreator'
|
||||
import { proxy, ref } from 'valtio'
|
||||
import { getItemDefinition } from 'mc-assets/dist/itemDefinitions'
|
||||
import { versionToNumber } from '../prepare/utils'
|
||||
|
||||
export const activeGuiAtlas = proxy({
|
||||
atlas: null as null | { json, image },
|
||||
})
|
||||
|
||||
export const getNonFullBlocksModels = () => {
|
||||
let version = appViewer.resourcesManager.currentResources!.version ?? 'latest'
|
||||
let version = viewer.world.texturesVersion ?? 'latest'
|
||||
if (versionToNumber(version) < versionToNumber('1.13')) version = '1.13'
|
||||
const itemsDefinitions = appViewer.resourcesManager.itemsDefinitionsStore.data.latest
|
||||
const itemsDefinitions = viewer.world.itemsDefinitionsStore.data.latest
|
||||
const blockModelsResolved = {} as Record<string, any>
|
||||
const itemsModelsResolved = {} as Record<string, any>
|
||||
const fullBlocksWithNonStandardDisplay = [] as string[]
|
||||
const handledItemsWithDefinitions = new Set()
|
||||
const assetsParser = new AssetsParser(version, getLoadedBlockstatesStore(appViewer.resourcesManager.currentResources!.blockstatesModels), getLoadedModelsStore(appViewer.resourcesManager.currentResources!.blockstatesModels))
|
||||
const assetsParser = new AssetsParser(version, getLoadedBlockstatesStore(viewer.world.blockstatesModels), getLoadedModelsStore(viewer.world.blockstatesModels))
|
||||
|
||||
const standardGuiDisplay = {
|
||||
'rotation': [
|
||||
|
|
@ -43,15 +48,13 @@ export const getNonFullBlocksModels = () => {
|
|||
if (!model?.elements?.length) return
|
||||
const isFullBlock = model.elements.length === 1 && arrEqual(model.elements[0].from, [0, 0, 0]) && arrEqual(model.elements[0].to, [16, 16, 16])
|
||||
if (isFullBlock) return
|
||||
const hasBetterPrerender = assetsParser.blockModelsStore.data.latest[`item/${name}`]?.textures?.['layer0']?.startsWith('invsprite_')
|
||||
if (hasBetterPrerender) return
|
||||
model['display'] ??= {}
|
||||
model['display']['gui'] ??= standardGuiDisplay
|
||||
blockModelsResolved[name] = model
|
||||
}
|
||||
|
||||
for (const [name, definition] of Object.entries(itemsDefinitions)) {
|
||||
const item = getItemDefinition(appViewer.resourcesManager.itemsDefinitionsStore, {
|
||||
const item = getItemDefinition(viewer.world.itemsDefinitionsStore, {
|
||||
version,
|
||||
name,
|
||||
properties: {
|
||||
|
|
@ -64,6 +67,7 @@ export const getNonFullBlocksModels = () => {
|
|||
handledItemsWithDefinitions.add(name)
|
||||
}
|
||||
if (resolvedModel?.elements) {
|
||||
|
||||
let hasStandardDisplay = true
|
||||
if (resolvedModel['display']?.gui) {
|
||||
hasStandardDisplay =
|
||||
|
|
@ -93,7 +97,7 @@ export const getNonFullBlocksModels = () => {
|
|||
}
|
||||
}
|
||||
|
||||
for (const [name, blockstate] of Object.entries(appViewer.resourcesManager.currentResources!.blockstatesModels.blockstates.latest)) {
|
||||
for (const [name, blockstate] of Object.entries(viewer.world.blockstatesModels.blockstates.latest)) {
|
||||
if (handledItemsWithDefinitions.has(name)) {
|
||||
continue
|
||||
}
|
||||
|
|
@ -116,19 +120,18 @@ export const getNonFullBlocksModels = () => {
|
|||
const RENDER_SIZE = 64
|
||||
|
||||
const generateItemsGui = async (models: Record<string, BlockModelMcAssets>, isItems = false) => {
|
||||
const { currentResources } = appViewer.resourcesManager
|
||||
const imgBitmap = isItems ? currentResources!.itemsAtlasImage : currentResources!.blocksAtlasImage
|
||||
const img = await getLoadedImage(isItems ? viewer.world.itemsAtlasParser!.latestImage : viewer.world.blocksAtlasParser!.latestImage)
|
||||
const canvasTemp = document.createElement('canvas')
|
||||
canvasTemp.width = imgBitmap.width
|
||||
canvasTemp.height = imgBitmap.height
|
||||
canvasTemp.width = img.width
|
||||
canvasTemp.height = img.height
|
||||
canvasTemp.style.imageRendering = 'pixelated'
|
||||
const ctx = canvasTemp.getContext('2d')!
|
||||
ctx.imageSmoothingEnabled = false
|
||||
ctx.drawImage(imgBitmap, 0, 0)
|
||||
ctx.drawImage(img, 0, 0)
|
||||
|
||||
const atlasParser = isItems ? appViewer.resourcesManager.itemsAtlasParser : appViewer.resourcesManager.blocksAtlasParser
|
||||
const atlasParser = isItems ? viewer.world.itemsAtlasParser! : viewer.world.blocksAtlasParser!
|
||||
const textureAtlas = new TextureAtlas(
|
||||
ctx.getImageData(0, 0, imgBitmap.width, imgBitmap.height),
|
||||
ctx.getImageData(0, 0, img.width, img.height),
|
||||
Object.fromEntries(Object.entries(atlasParser.atlas.latest.textures).map(([key, value]) => {
|
||||
return [key, [
|
||||
value.u,
|
||||
|
|
@ -142,7 +145,6 @@ const generateItemsGui = async (models: Record<string, BlockModelMcAssets>, isIt
|
|||
const PREVIEW_ID = Identifier.parse('preview:preview')
|
||||
const PREVIEW_DEFINITION = new BlockDefinition({ '': { model: PREVIEW_ID.toString() } }, undefined)
|
||||
|
||||
let textureWasRequested = false
|
||||
let modelData: any
|
||||
let currentModelName: string | undefined
|
||||
const resources: ItemRendererResources = {
|
||||
|
|
@ -153,7 +155,6 @@ const generateItemsGui = async (models: Record<string, BlockModelMcAssets>, isIt
|
|||
return null
|
||||
},
|
||||
getTextureUV (texture) {
|
||||
textureWasRequested = true
|
||||
return textureAtlas.getTextureUV(texture.toString().replace('minecraft:', '').replace('block/', '').replace('item/', '').replace('blocks/', '').replace('items/', '') as any)
|
||||
},
|
||||
getTextureAtlas () {
|
||||
|
|
@ -202,7 +203,6 @@ const generateItemsGui = async (models: Record<string, BlockModelMcAssets>, isIt
|
|||
const renderer = new ItemRenderer(gl, item, resources, { display_context: 'gui' })
|
||||
const missingTextures = new Set()
|
||||
for (const [modelName, model] of Object.entries(models)) {
|
||||
textureWasRequested = false
|
||||
if (includeOnly.length && !includeOnly.includes(modelName)) continue
|
||||
|
||||
const patchMissingTextures = () => {
|
||||
|
|
@ -224,7 +224,6 @@ const generateItemsGui = async (models: Record<string, BlockModelMcAssets>, isIt
|
|||
if (!modelData) continue
|
||||
renderer.setItem(item, { display_context: 'gui' })
|
||||
renderer.drawItem()
|
||||
if (!textureWasRequested) continue
|
||||
const url = canvas.toDataURL()
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const img = await getLoadedImage(url)
|
||||
|
|
@ -238,9 +237,6 @@ const generateItemsGui = async (models: Record<string, BlockModelMcAssets>, isIt
|
|||
return images
|
||||
}
|
||||
|
||||
/**
|
||||
* @mainThread
|
||||
*/
|
||||
const generateAtlas = async (images: Record<string, HTMLImageElement>) => {
|
||||
const atlas = makeTextureAtlas({
|
||||
input: Object.keys(images),
|
||||
|
|
@ -258,9 +254,9 @@ const generateAtlas = async (images: Record<string, HTMLImageElement>) => {
|
|||
// a.download = 'blocks_atlas.png'
|
||||
// a.click()
|
||||
|
||||
appViewer.resourcesManager.currentResources!.guiAtlas = {
|
||||
activeGuiAtlas.atlas = {
|
||||
json: atlas.json,
|
||||
image: await createImageBitmap(atlas.canvas),
|
||||
image: ref(await getLoadedImage(atlas.canvas.toDataURL())),
|
||||
}
|
||||
|
||||
return atlas
|
||||
|
|
@ -277,6 +273,5 @@ export const generateGuiAtlas = async () => {
|
|||
const itemImages = await generateItemsGui(itemsModelsResolved, true)
|
||||
console.timeEnd('generate items gui atlas')
|
||||
await generateAtlas({ ...blockImages, ...itemImages })
|
||||
appViewer.resourcesManager.currentResources!.guiAtlasVersion++
|
||||
// await generateAtlas(blockImages)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import * as THREE from 'three'
|
||||
import { loadSkinFromUsername, loadSkinImage } from '../lib/utils/skins'
|
||||
import { steveTexture } from './entities'
|
||||
|
||||
import { loadSkinToCanvas } from 'skinview-utils'
|
||||
import { getLookupUrl, loadSkinImage, steveTexture } from './utils/skins'
|
||||
|
||||
export const getMyHand = async (image?: string, userName?: string) => {
|
||||
let newMap: THREE.Texture
|
||||
|
|
@ -9,10 +8,7 @@ export const getMyHand = async (image?: string, userName?: string) => {
|
|||
newMap = await steveTexture
|
||||
} else {
|
||||
if (!image) {
|
||||
image = await loadSkinFromUsername(userName!, 'skin')
|
||||
}
|
||||
if (!image) {
|
||||
return
|
||||
image = getLookupUrl(userName!, 'skin')
|
||||
}
|
||||
const { canvas } = await loadSkinImage(image)
|
||||
newMap = new THREE.CanvasTexture(canvas)
|
||||
|
|
@ -1,17 +1,15 @@
|
|||
import * as THREE from 'three'
|
||||
import * as tweenJs from '@tweenjs/tween.js'
|
||||
import PrismarineItem from 'prismarine-item'
|
||||
import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
|
||||
import worldBlockProvider from 'mc-assets/dist/worldBlockProvider'
|
||||
import { BlockModel } from 'mc-assets'
|
||||
import { getThreeBlockModelGroup, renderBlockThree, setBlockPosition } from '../lib/mesher/standaloneRenderer'
|
||||
import { MovementState, PlayerStateRenderer } from '../lib/basePlayerState'
|
||||
import { DebugGui } from '../lib/DebugGui'
|
||||
import { SmoothSwitcher } from '../lib/smoothSwitcher'
|
||||
import { watchProperty } from '../lib/utils/proxy'
|
||||
import { WorldRendererConfig } from '../lib/worldrendererCommon'
|
||||
import { getThreeBlockModelGroup, renderBlockThree, setBlockPosition } from './mesher/standaloneRenderer'
|
||||
import { getMyHand } from './hand'
|
||||
import { WorldRendererThree } from './worldrendererThree'
|
||||
import { IPlayerState, MovementState } from './basePlayerState'
|
||||
import { DebugGui } from './DebugGui'
|
||||
import { SmoothSwitcher } from './smoothSwitcher'
|
||||
import { watchProperty } from './utils/proxy'
|
||||
import { disposeObject } from './threeJsUtils'
|
||||
import { WorldRendererConfig } from './worldrendererCommon'
|
||||
|
||||
export type HandItemBlock = {
|
||||
name?
|
||||
|
|
@ -116,61 +114,41 @@ export default class HoldingBlock {
|
|||
offHandModeLegacy = false
|
||||
|
||||
swingAnimator: HandSwingAnimator | undefined
|
||||
config: WorldRendererConfig
|
||||
|
||||
constructor (public worldRenderer: WorldRendererThree, public offHand = false) {
|
||||
constructor (public playerState: IPlayerState, public config: WorldRendererConfig, public offHand = false) {
|
||||
this.initCameraGroup()
|
||||
this.worldRenderer.onReactivePlayerStateUpdated('heldItemMain', () => {
|
||||
if (!this.offHand) {
|
||||
this.updateItem()
|
||||
}
|
||||
}, false)
|
||||
this.worldRenderer.onReactivePlayerStateUpdated('heldItemOff', () => {
|
||||
if (this.offHand) {
|
||||
this.updateItem()
|
||||
}
|
||||
}, false)
|
||||
this.config = worldRenderer.displayOptions.inWorldRenderingConfig
|
||||
|
||||
this.playerState.events.on('heldItemChanged', (_, isOffHand) => {
|
||||
if (this.offHand !== isOffHand) return
|
||||
this.updateItem()
|
||||
})
|
||||
|
||||
this.offHandDisplay = this.offHand
|
||||
// this.offHandDisplay = true
|
||||
if (!this.offHand) {
|
||||
// load default hand
|
||||
void getMyHand().then((hand) => {
|
||||
this.playerHand = hand
|
||||
// trigger update
|
||||
this.updateItem()
|
||||
}).then(() => {
|
||||
// now watch over the player skin
|
||||
watchProperty(
|
||||
async () => {
|
||||
return getMyHand(this.worldRenderer.playerStateReactive.playerSkin, this.worldRenderer.playerStateReactive.onlineMode ? this.worldRenderer.playerStateReactive.username : undefined)
|
||||
},
|
||||
this.worldRenderer.playerStateReactive,
|
||||
'playerSkin',
|
||||
(newHand) => {
|
||||
if (newHand) {
|
||||
this.playerHand = newHand
|
||||
// trigger update
|
||||
this.updateItem()
|
||||
}
|
||||
},
|
||||
(oldHand) => {
|
||||
disposeObject(oldHand!, true)
|
||||
}
|
||||
)
|
||||
})
|
||||
// watch over my hand
|
||||
watchProperty(
|
||||
async () => {
|
||||
return getMyHand(this.playerState.reactive.playerSkin, this.playerState.onlineMode ? this.playerState.username : undefined)
|
||||
},
|
||||
this.playerState.reactive,
|
||||
'playerSkin',
|
||||
(newHand) => {
|
||||
this.playerHand = newHand
|
||||
},
|
||||
(oldHand) => {
|
||||
disposeObject(oldHand, true)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
updateItem () {
|
||||
if (!this.ready) return
|
||||
const item = this.offHand ? this.worldRenderer.playerStateReactive.heldItemOff : this.worldRenderer.playerStateReactive.heldItemMain
|
||||
if (!this.ready || !this.playerState.getHeldItem) return
|
||||
const item = this.playerState.getHeldItem(this.offHand)
|
||||
if (item) {
|
||||
void this.setNewItem(item)
|
||||
} else if (this.offHand) {
|
||||
void this.setNewItem()
|
||||
} else {
|
||||
} else if (!this.offHand) {
|
||||
void this.setNewItem({
|
||||
type: 'hand',
|
||||
})
|
||||
|
|
@ -302,7 +280,6 @@ export default class HoldingBlock {
|
|||
}
|
||||
|
||||
isDifferentItem (block: HandItemBlock | undefined) {
|
||||
const Item = PrismarineItem(this.worldRenderer.version)
|
||||
if (!this.lastHeldItem) {
|
||||
return true
|
||||
}
|
||||
|
|
@ -310,7 +287,7 @@ export default class HoldingBlock {
|
|||
return true
|
||||
}
|
||||
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
|
||||
if (!Item.equal(this.lastHeldItem.fullItem, block?.fullItem ?? {}) || JSON.stringify(this.lastHeldItem.fullItem.components) !== JSON.stringify(block?.fullItem?.components)) {
|
||||
if (JSON.stringify(this.lastHeldItem.fullItem) !== JSON.stringify(block?.fullItem ?? '{}')) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -350,14 +327,14 @@ export default class HoldingBlock {
|
|||
|
||||
let blockInner: THREE.Object3D | undefined
|
||||
if (handItem.type === 'item' || handItem.type === 'block') {
|
||||
const result = this.worldRenderer.entities.getItemMesh({
|
||||
const result = viewer.entities.getItemMesh({
|
||||
...handItem.fullItem,
|
||||
itemId: handItem.id,
|
||||
}, {
|
||||
'minecraft:display_context': 'firstperson',
|
||||
'minecraft:use_duration': this.worldRenderer.playerStateReactive.itemUsageTicks,
|
||||
'minecraft:using_item': !!this.worldRenderer.playerStateReactive.itemUsageTicks,
|
||||
}, false, this.lastItemModelName)
|
||||
'minecraft:use_duration': this.playerState.getItemUsageTicks?.(),
|
||||
'minecraft:using_item': !!this.playerState.getItemUsageTicks?.(),
|
||||
}, this.lastItemModelName)
|
||||
if (result) {
|
||||
const { mesh: itemMesh, isBlock, modelName } = result
|
||||
if (isBlock) {
|
||||
|
|
@ -473,7 +450,7 @@ export default class HoldingBlock {
|
|||
this.swingAnimator = new HandSwingAnimator(this.holdingBlockInnerGroup)
|
||||
this.swingAnimator.type = result.type
|
||||
if (this.config.viewBobbing) {
|
||||
this.idleAnimator = new HandIdleAnimator(this.holdingBlockInnerGroup, this.worldRenderer.playerStateReactive)
|
||||
this.idleAnimator = new HandIdleAnimator(this.holdingBlockInnerGroup, this.playerState)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -554,7 +531,7 @@ class HandIdleAnimator {
|
|||
|
||||
private readonly debugGui: DebugGui
|
||||
|
||||
constructor (public handMesh: THREE.Object3D, public playerState: PlayerStateRenderer) {
|
||||
constructor (public handMesh: THREE.Object3D, public playerState: IPlayerState) {
|
||||
this.handMesh = handMesh
|
||||
this.globalTime = 0
|
||||
this.currentState = 'NOT_MOVING'
|
||||
|
|
@ -708,7 +685,7 @@ class HandIdleAnimator {
|
|||
|
||||
// Check for state changes from player state
|
||||
if (this.playerState) {
|
||||
const newState = this.playerState.movementState
|
||||
const newState = this.playerState.getMovementState()
|
||||
if (newState !== this.targetState) {
|
||||
this.setState(newState)
|
||||
}
|
||||
|
|
@ -924,10 +901,11 @@ class HandSwingAnimator {
|
|||
}
|
||||
}
|
||||
|
||||
export const getBlockMeshFromModel = (material: THREE.Material, model: BlockModel, name: string, blockProvider: WorldBlockProvider) => {
|
||||
export const getBlockMeshFromModel = (material: THREE.Material, model: BlockModel, name: string) => {
|
||||
const blockProvider = worldBlockProvider(viewer.world.blockstatesModels, viewer.world.blocksAtlasParser!.atlas, 'latest')
|
||||
const worldRenderModel = blockProvider.transformModel(model, {
|
||||
name,
|
||||
properties: {}
|
||||
}) as any
|
||||
})
|
||||
return getThreeBlockModelGroup(material, [[worldRenderModel]], undefined, 'plains', loadedData)
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ import { Vec3 } from 'vec3'
|
|||
import { World } from './world'
|
||||
import { getSectionGeometry, setBlockStatesData as setMesherData } from './models'
|
||||
import { BlockStateModelInfo } from './shared'
|
||||
import { INVISIBLE_BLOCKS } from './worldConstants'
|
||||
|
||||
globalThis.structuredClone ??= (value) => JSON.parse(JSON.stringify(value))
|
||||
|
||||
|
|
@ -54,7 +53,7 @@ function setSectionDirty (pos, value = true) {
|
|||
const key = sectionKey(x, y, z)
|
||||
if (!value) {
|
||||
dirtySections.delete(key)
|
||||
postMessage({ type: 'sectionFinished', key, workerIndex })
|
||||
postMessage({ type: 'sectionFinished', key })
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -62,7 +61,7 @@ function setSectionDirty (pos, value = true) {
|
|||
if (chunk?.getSection(pos)) {
|
||||
dirtySections.set(key, (dirtySections.get(key) || 0) + 1)
|
||||
} else {
|
||||
postMessage({ type: 'sectionFinished', key, workerIndex })
|
||||
postMessage({ type: 'sectionFinished', key })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -77,7 +76,6 @@ const handleMessage = data => {
|
|||
|
||||
if (data.type === 'mcData') {
|
||||
globalVar.mcData = data.mcData
|
||||
globalVar.loadedData = data.mcData
|
||||
}
|
||||
|
||||
if (data.config) {
|
||||
|
|
@ -123,13 +121,11 @@ const handleMessage = data => {
|
|||
}
|
||||
case 'blockUpdate': {
|
||||
const loc = new Vec3(data.pos.x, data.pos.y, data.pos.z).floored()
|
||||
if (data.stateId !== undefined && data.stateId !== null) {
|
||||
world?.setBlockStateId(loc, data.stateId)
|
||||
}
|
||||
world.setBlockStateId(loc, data.stateId)
|
||||
|
||||
const chunkKey = `${Math.floor(loc.x / 16) * 16},${Math.floor(loc.z / 16) * 16}`
|
||||
if (data.customBlockModels) {
|
||||
world?.customBlockModels.set(chunkKey, data.customBlockModels)
|
||||
world.customBlockModels.set(chunkKey, data.customBlockModels)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
|
@ -139,40 +135,8 @@ const handleMessage = data => {
|
|||
dirtySections = new Map()
|
||||
// todo also remove cached
|
||||
globalVar.mcData = null
|
||||
globalVar.loadedData = null
|
||||
allDataReady = false
|
||||
|
||||
break
|
||||
}
|
||||
case 'getCustomBlockModel': {
|
||||
const pos = new Vec3(data.pos.x, data.pos.y, data.pos.z)
|
||||
const chunkKey = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.z / 16) * 16}`
|
||||
const customBlockModel = world.customBlockModels.get(chunkKey)?.[`${pos.x},${pos.y},${pos.z}`]
|
||||
global.postMessage({ type: 'customBlockModel', chunkKey, customBlockModel })
|
||||
break
|
||||
}
|
||||
case 'getHeightmap': {
|
||||
const heightmap = new Uint8Array(256)
|
||||
|
||||
const blockPos = new Vec3(0, 0, 0)
|
||||
for (let z = 0; z < 16; z++) {
|
||||
for (let x = 0; x < 16; x++) {
|
||||
const blockX = x + data.x
|
||||
const blockZ = z + data.z
|
||||
blockPos.x = blockX
|
||||
blockPos.z = blockZ
|
||||
blockPos.y = world.config.worldMaxY
|
||||
let block = world.getBlock(blockPos)
|
||||
while (block && INVISIBLE_BLOCKS.has(block.name) && blockPos.y > world.config.worldMinY) {
|
||||
blockPos.y -= 1
|
||||
block = world.getBlock(blockPos)
|
||||
}
|
||||
const index = z * 16 + x
|
||||
heightmap[index] = block ? blockPos.y : 0
|
||||
}
|
||||
}
|
||||
postMessage({ type: 'heightmap', key: `${Math.floor(data.x / 16)},${Math.floor(data.z / 16)}`, heightmap })
|
||||
|
||||
break
|
||||
}
|
||||
// No default
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Vec3 } from 'vec3'
|
|||
import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
|
||||
import legacyJson from '../../../../src/preflatMap.json'
|
||||
import { BlockType } from '../../../playground/shared'
|
||||
import { World, BlockModelPartsResolved, WorldBlock as Block, WorldBlock } from './world'
|
||||
import { World, BlockModelPartsResolved, WorldBlock as Block } from './world'
|
||||
import { BlockElement, buildRotationMatrix, elemFaces, matmul3, matmulmat3, vecadd3, vecsub3 } from './modelsGeometryCommon'
|
||||
import { INVISIBLE_BLOCKS } from './worldConstants'
|
||||
import { MesherGeometryOutput, HighestBlockInfo } from './shared'
|
||||
|
|
@ -103,8 +103,7 @@ function tintToGl (tint) {
|
|||
return [r / 255, g / 255, b / 255]
|
||||
}
|
||||
|
||||
function getLiquidRenderHeight (world: World, block: WorldBlock | null, type: number, pos: Vec3, isWater: boolean, isRealWater: boolean) {
|
||||
if ((isWater && !isRealWater) || (block && isBlockWaterlogged(block))) return 8 / 9
|
||||
function getLiquidRenderHeight (world, block, type, pos) {
|
||||
if (!block || block.type !== type) return 1 / 9
|
||||
if (block.metadata === 0) { // source block
|
||||
const blockAbove = world.getBlock(pos.offset(0, 1, 0))
|
||||
|
|
@ -125,19 +124,12 @@ const isCube = (block: Block) => {
|
|||
}))
|
||||
}
|
||||
|
||||
const getVec = (v: Vec3, dir: Vec3) => {
|
||||
for (const coord of ['x', 'y', 'z']) {
|
||||
if (Math.abs(dir[coord]) > 0) v[coord] = 0
|
||||
}
|
||||
return v.plus(dir)
|
||||
}
|
||||
|
||||
function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, type: number, biome: string, water: boolean, attr: MesherGeometryOutput, isRealWater: boolean) {
|
||||
function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, type: number, biome: string, water: boolean, attr: Record<string, any>) {
|
||||
const heights: number[] = []
|
||||
for (let z = -1; z <= 1; z++) {
|
||||
for (let x = -1; x <= 1; x++) {
|
||||
const pos = cursor.offset(x, 0, z)
|
||||
heights.push(getLiquidRenderHeight(world, world.getBlock(pos), type, pos, water, isRealWater))
|
||||
heights.push(getLiquidRenderHeight(world, world.getBlock(pos), type, pos))
|
||||
}
|
||||
}
|
||||
const cornerHeights = [
|
||||
|
|
@ -149,14 +141,15 @@ function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, typ
|
|||
|
||||
// eslint-disable-next-line guard-for-in
|
||||
for (const face in elemFaces) {
|
||||
const { dir, corners, mask1, mask2 } = elemFaces[face]
|
||||
const { dir, corners } = elemFaces[face]
|
||||
const isUp = dir[1] === 1
|
||||
|
||||
const neighborPos = cursor.offset(...dir as [number, number, number])
|
||||
const neighbor = world.getBlock(neighborPos)
|
||||
if (!neighbor) continue
|
||||
if (neighbor.type === type || (water && (neighbor.name === 'water' || isBlockWaterlogged(neighbor)))) continue
|
||||
if (isCube(neighbor) && !isUp) continue
|
||||
if (neighbor.type === type) continue
|
||||
const isGlass = neighbor.name.includes('glass')
|
||||
if ((isCube(neighbor) && !isUp) || neighbor.material === 'plant' || neighbor.getProperties().waterlogged) continue
|
||||
|
||||
let tint = [1, 1, 1]
|
||||
if (water) {
|
||||
|
|
@ -187,44 +180,16 @@ function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, typ
|
|||
const { su } = texture
|
||||
const { sv } = texture
|
||||
|
||||
// Get base light value for the face
|
||||
const baseLight = world.getLight(neighborPos, undefined, undefined, water ? 'water' : 'lava') / 15
|
||||
|
||||
for (const pos of corners) {
|
||||
const height = cornerHeights[pos[2] * 2 + pos[0]]
|
||||
const OFFSET = 0.0001
|
||||
attr.t_positions!.push(
|
||||
(pos[0] ? 1 - OFFSET : OFFSET) + (cursor.x & 15) - 8,
|
||||
(pos[1] ? height - OFFSET : OFFSET) + (cursor.y & 15) - 8,
|
||||
(pos[2] ? 1 - OFFSET : OFFSET) + (cursor.z & 15) - 8
|
||||
attr.t_positions.push(
|
||||
(pos[0] ? 0.999 : 0.001) + (cursor.x & 15) - 8,
|
||||
(pos[1] ? height - 0.001 : 0.001) + (cursor.y & 15) - 8,
|
||||
(pos[2] ? 0.999 : 0.001) + (cursor.z & 15) - 8
|
||||
)
|
||||
attr.t_normals!.push(...dir)
|
||||
attr.t_uvs!.push(pos[3] * su + u, pos[4] * sv * (pos[1] ? 1 : height) + v)
|
||||
|
||||
let cornerLightResult = baseLight
|
||||
if (world.config.smoothLighting) {
|
||||
const dx = pos[0] * 2 - 1
|
||||
const dy = pos[1] * 2 - 1
|
||||
const dz = pos[2] * 2 - 1
|
||||
const cornerDir: [number, number, number] = [dx, dy, dz]
|
||||
const side1Dir: [number, number, number] = [dx * mask1[0], dy * mask1[1], dz * mask1[2]]
|
||||
const side2Dir: [number, number, number] = [dx * mask2[0], dy * mask2[1], dz * mask2[2]]
|
||||
|
||||
const dirVec = new Vec3(...dir as [number, number, number])
|
||||
|
||||
const side1LightDir = getVec(new Vec3(...side1Dir), dirVec)
|
||||
const side1Light = world.getLight(cursor.plus(side1LightDir)) / 15
|
||||
const side2DirLight = getVec(new Vec3(...side2Dir), dirVec)
|
||||
const side2Light = world.getLight(cursor.plus(side2DirLight)) / 15
|
||||
const cornerLightDir = getVec(new Vec3(...cornerDir), dirVec)
|
||||
const cornerLight = world.getLight(cursor.plus(cornerLightDir)) / 15
|
||||
// interpolate
|
||||
const lights = [side1Light, side2Light, cornerLight, baseLight]
|
||||
cornerLightResult = lights.reduce((acc, cur) => acc + cur, 0) / lights.length
|
||||
}
|
||||
|
||||
// Apply light value to tint
|
||||
attr.t_colors!.push(tint[0] * cornerLightResult, tint[1] * cornerLightResult, tint[2] * cornerLightResult)
|
||||
attr.t_normals.push(...dir)
|
||||
attr.t_uvs.push(pos[3] * su + u, pos[4] * sv * (pos[1] ? 1 : height) + v)
|
||||
attr.t_colors.push(tint[0], tint[1], tint[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -288,7 +253,7 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO:
|
|||
if (!neighbor.transparent && (isCube(neighbor) || identicalCull(element, neighbor, new Vec3(...dir)))) continue
|
||||
} else {
|
||||
needSectionRecomputeOnChange = true
|
||||
// continue
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -336,7 +301,7 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO:
|
|||
let localShift = null as any
|
||||
|
||||
if (element.rotation && !needTiles) {
|
||||
// Rescale support for block model rotations
|
||||
// todo do we support rescale?
|
||||
localMatrix = buildRotationMatrix(
|
||||
element.rotation.axis,
|
||||
element.rotation.angle
|
||||
|
|
@ -349,37 +314,6 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO:
|
|||
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[] = []
|
||||
|
|
@ -489,19 +423,13 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO:
|
|||
|
||||
if (!needTiles) {
|
||||
if (doAO && aos[0] + aos[3] >= aos[1] + aos[2]) {
|
||||
attr.indices[attr.indicesCount++] = ndx
|
||||
attr.indices[attr.indicesCount++] = ndx + 3
|
||||
attr.indices[attr.indicesCount++] = ndx + 2
|
||||
attr.indices[attr.indicesCount++] = ndx
|
||||
attr.indices[attr.indicesCount++] = ndx + 1
|
||||
attr.indices[attr.indicesCount++] = ndx + 3
|
||||
attr.indices.push(
|
||||
ndx, ndx + 3, ndx + 2, ndx, ndx + 1, ndx + 3
|
||||
)
|
||||
} else {
|
||||
attr.indices[attr.indicesCount++] = ndx
|
||||
attr.indices[attr.indicesCount++] = ndx + 1
|
||||
attr.indices[attr.indicesCount++] = ndx + 2
|
||||
attr.indices[attr.indicesCount++] = ndx + 2
|
||||
attr.indices[attr.indicesCount++] = ndx + 1
|
||||
attr.indices[attr.indicesCount++] = ndx + 3
|
||||
attr.indices.push(
|
||||
ndx, ndx + 1, ndx + 2, ndx + 2, ndx + 1, ndx + 3
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -519,7 +447,7 @@ const isBlockWaterlogged = (block: Block) => {
|
|||
}
|
||||
|
||||
let unknownBlockModel: BlockModelPartsResolved
|
||||
export function getSectionGeometry (sx: number, sy: number, sz: number, world: World) {
|
||||
export function getSectionGeometry (sx, sy, sz, world: World) {
|
||||
let delayedRender = [] as Array<() => void>
|
||||
|
||||
const attr: MesherGeometryOutput = {
|
||||
|
|
@ -535,13 +463,12 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W
|
|||
t_colors: [],
|
||||
t_uvs: [],
|
||||
indices: [],
|
||||
indicesCount: 0, // Track current index position
|
||||
using32Array: true,
|
||||
tiles: {},
|
||||
// todo this can be removed here
|
||||
heads: {},
|
||||
signs: {},
|
||||
// isFull: true,
|
||||
highestBlocks: new Map<string, HighestBlockInfo>([]),
|
||||
hadErrors: false,
|
||||
blocksCount: 0
|
||||
}
|
||||
|
|
@ -551,6 +478,12 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W
|
|||
for (cursor.z = sz; cursor.z < sz + 16; cursor.z++) {
|
||||
for (cursor.x = sx; cursor.x < sx + 16; cursor.x++) {
|
||||
let block = world.getBlock(cursor, blockProvider, attr)!
|
||||
if (!INVISIBLE_BLOCKS.has(block.name)) {
|
||||
const highest = attr.highestBlocks.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 ((block.name.includes('_sign') || block.name === 'sign') && !world.config.disableSignsMapsSupport) {
|
||||
const key = `${cursor.x},${cursor.y},${cursor.z}`
|
||||
|
|
@ -606,11 +539,11 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W
|
|||
const pos = cursor.clone()
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
delayedRender.push(() => {
|
||||
renderLiquid(world, pos, blockProvider.getTextureInfo('water_still'), block.type, biome, true, attr, !isWaterlogged)
|
||||
renderLiquid(world, pos, blockProvider.getTextureInfo('water_still'), block.type, biome, true, attr)
|
||||
})
|
||||
attr.blocksCount++
|
||||
} else if (block.name === 'lava') {
|
||||
renderLiquid(world, cursor, blockProvider.getTextureInfo('lava_still'), block.type, biome, false, attr, false)
|
||||
renderLiquid(world, cursor, blockProvider.getTextureInfo('lava_still'), block.type, biome, false, attr)
|
||||
attr.blocksCount++
|
||||
}
|
||||
if (block.name !== 'water' && block.name !== 'lava' && !INVISIBLE_BLOCKS.has(block.name)) {
|
||||
|
|
@ -672,19 +605,12 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W
|
|||
|
||||
let ndx = attr.positions.length / 3
|
||||
for (let i = 0; i < attr.t_positions!.length / 12; i++) {
|
||||
attr.indices[attr.indicesCount++] = ndx
|
||||
attr.indices[attr.indicesCount++] = ndx + 1
|
||||
attr.indices[attr.indicesCount++] = ndx + 2
|
||||
attr.indices[attr.indicesCount++] = ndx + 2
|
||||
attr.indices[attr.indicesCount++] = ndx + 1
|
||||
attr.indices[attr.indicesCount++] = ndx + 3
|
||||
// back face
|
||||
attr.indices[attr.indicesCount++] = ndx
|
||||
attr.indices[attr.indicesCount++] = ndx + 2
|
||||
attr.indices[attr.indicesCount++] = ndx + 1
|
||||
attr.indices[attr.indicesCount++] = ndx + 2
|
||||
attr.indices[attr.indicesCount++] = ndx + 3
|
||||
attr.indices[attr.indicesCount++] = ndx + 1
|
||||
attr.indices.push(
|
||||
ndx, ndx + 1, ndx + 2, ndx + 2, ndx + 1, ndx + 3,
|
||||
// eslint-disable-next-line @stylistic/function-call-argument-newline
|
||||
// back face
|
||||
ndx, ndx + 2, ndx + 1, ndx + 2, ndx + 3, ndx + 1
|
||||
)
|
||||
ndx += 4
|
||||
}
|
||||
|
||||
|
|
@ -702,12 +628,6 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W
|
|||
attr.normals = new Float32Array(attr.normals) as any
|
||||
attr.colors = new Float32Array(attr.colors) as any
|
||||
attr.uvs = new Float32Array(attr.uvs) as any
|
||||
attr.using32Array = arrayNeedsUint32(attr.indices)
|
||||
if (attr.using32Array) {
|
||||
attr.indices = new Uint32Array(attr.indices)
|
||||
} else {
|
||||
attr.indices = new Uint16Array(attr.indices)
|
||||
}
|
||||
|
||||
if (needTiles) {
|
||||
delete attr.positions
|
||||
|
|
@ -719,21 +639,6 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W
|
|||
return attr
|
||||
}
|
||||
|
||||
// copied from three.js
|
||||
function arrayNeedsUint32 (array) {
|
||||
|
||||
// assumes larger values usually on last
|
||||
|
||||
for (let i = array.length - 1; i >= 0; -- i) {
|
||||
|
||||
if (array[i] >= 65_535) return true // account for PRIMITIVE_RESTART_FIXED_INDEX, #24565
|
||||
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
export const setBlockStatesData = (blockstatesModels, blocksAtlas: any, _needTiles = false, useUnknownBlockModel = true, version = 'latest') => {
|
||||
blockProvider = worldBlockProvider(blockstatesModels, blocksAtlas, version)
|
||||
globalThis.blockProvider = blockProvider
|
||||
|
|
|
|||
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